feat: download and preview files from local backups automatically, if a local backup is available (#2076)
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import { FileBackupRecord, FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import {
|
||||
FileBackupRecord,
|
||||
FileBackupsDevice,
|
||||
FileBackupsMapping,
|
||||
FileBackupReadToken,
|
||||
FileBackupReadChunkResponse,
|
||||
} from '@web/Application/Device/DesktopSnjsExports'
|
||||
import { AppState } from 'app/AppState'
|
||||
import { shell } from 'electron'
|
||||
import { StoreKeys } from '../Store/StoreKeys'
|
||||
@@ -6,13 +12,14 @@ import path from 'path'
|
||||
import {
|
||||
deleteFile,
|
||||
ensureDirectoryExists,
|
||||
moveFiles,
|
||||
moveDirContents,
|
||||
openDirectoryPicker,
|
||||
readJSONFile,
|
||||
writeFile,
|
||||
writeJSONFile,
|
||||
} from '../Utils/FileUtils'
|
||||
import { FileDownloader } from './FileDownloader'
|
||||
import { FileReadOperation } from './FileReadOperation'
|
||||
|
||||
export const FileBackupsConstantsV1 = {
|
||||
Version: '1.0.0',
|
||||
@@ -21,6 +28,8 @@ export const FileBackupsConstantsV1 = {
|
||||
}
|
||||
|
||||
export class FilesBackupManager implements FileBackupsDevice {
|
||||
private readOperations: Map<string, FileReadOperation> = new Map()
|
||||
|
||||
constructor(private appState: AppState) {}
|
||||
|
||||
public isFilesBackupsEnabled(): Promise<boolean> {
|
||||
@@ -78,8 +87,11 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
}
|
||||
|
||||
const entries = Object.values(mapping.files)
|
||||
const itemFolders = entries.map((entry) => path.join(oldPath, entry.relativePath))
|
||||
await moveFiles(itemFolders, newPath)
|
||||
for (const entry of entries) {
|
||||
const sourcePath = path.join(oldPath, entry.relativePath)
|
||||
const destinationPath = path.join(newPath, entry.relativePath)
|
||||
await moveDirContents(sourcePath, destinationPath)
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
entry.absolutePath = path.join(newPath, entry.relativePath)
|
||||
@@ -188,4 +200,28 @@ export class FilesBackupManager implements FileBackupsDevice {
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async getFileBackupReadToken(record: FileBackupRecord): Promise<FileBackupReadToken> {
|
||||
const operation = new FileReadOperation(record)
|
||||
|
||||
this.readOperations.set(operation.token, operation)
|
||||
|
||||
return operation.token
|
||||
}
|
||||
|
||||
async readNextChunk(token: string): Promise<FileBackupReadChunkResponse> {
|
||||
const operation = this.readOperations.get(token)
|
||||
|
||||
if (!operation) {
|
||||
return Promise.reject(new Error('Invalid token'))
|
||||
}
|
||||
|
||||
const result = await operation.readNextChunk()
|
||||
|
||||
if (result.isLast) {
|
||||
this.readOperations.delete(token)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FileBackupReadChunkResponse, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const ONE_MB = 1024 * 1024
|
||||
const CHUNK_LIMIT = ONE_MB * 5
|
||||
|
||||
export class FileReadOperation {
|
||||
public readonly token: string
|
||||
private currentChunkLocation = 0
|
||||
private localFileId: number
|
||||
private fileLength: number
|
||||
|
||||
constructor(backupRecord: FileBackupRecord) {
|
||||
this.token = backupRecord.absolutePath
|
||||
this.localFileId = fs.openSync(path.join(backupRecord.absolutePath, backupRecord.binaryFileName), 'r')
|
||||
this.fileLength = fs.fstatSync(this.localFileId).size
|
||||
}
|
||||
|
||||
async readNextChunk(): Promise<FileBackupReadChunkResponse> {
|
||||
let isLast = false
|
||||
let readUpto = this.currentChunkLocation + CHUNK_LIMIT
|
||||
if (readUpto > this.fileLength) {
|
||||
readUpto = this.fileLength
|
||||
isLast = true
|
||||
}
|
||||
|
||||
const readLength = readUpto - this.currentChunkLocation
|
||||
|
||||
const chunk = await this.readChunk(this.currentChunkLocation, readLength)
|
||||
|
||||
this.currentChunkLocation = readUpto
|
||||
|
||||
if (isLast) {
|
||||
fs.close(this.localFileId)
|
||||
}
|
||||
|
||||
return {
|
||||
chunk,
|
||||
isLast,
|
||||
progress: {
|
||||
encryptedFileSize: this.fileLength,
|
||||
encryptedBytesDownloaded: this.currentChunkLocation,
|
||||
encryptedBytesRemaining: this.fileLength - this.currentChunkLocation,
|
||||
percentComplete: (this.currentChunkLocation / this.fileLength) * 100.0,
|
||||
source: 'local',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async readChunk(start: number, length: number): Promise<Uint8Array> {
|
||||
const buffer = Buffer.alloc(length)
|
||||
|
||||
fs.readSync(this.localFileId, buffer, 0, length, start)
|
||||
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user