feat: download and preview files from local backups automatically, if a local backup is available (#2076)

This commit is contained in:
Mo
2022-12-01 11:56:28 -06:00
committed by GitHub
parent e07fed267f
commit 28e43d37c0
34 changed files with 739 additions and 110 deletions

View File

@@ -1,15 +1,14 @@
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { FileItem } from '@standardnotes/models'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { ChallengeServiceInterface } from '../Challenge'
import { InternalEventBusInterface } from '..'
import { AlertService } from '../Alert/AlertService'
import { ApiServiceInterface } from '../Api/ApiServiceInterface'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { FileService } from './FileService'
import { BackupServiceInterface } from '@standardnotes/files'
describe('fileService', () => {
let apiService: ApiServiceInterface
@@ -21,13 +20,33 @@ describe('fileService', () => {
let fileService: FileService
let encryptor: EncryptionProviderInterface
let internalEventBus: InternalEventBusInterface
let backupService: BackupServiceInterface
beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface>
apiService.addEventObserver = jest.fn()
apiService.createFileValetToken = jest.fn()
apiService.downloadFile = jest.fn()
apiService.deleteFile = jest.fn().mockReturnValue({})
const numChunks = 1
apiService.downloadFile = jest
.fn()
.mockImplementation(
(
_file: string,
_chunkIndex: number,
_apiToken: string,
_rangeStart: number,
onBytesReceived: (bytes: Uint8Array) => void,
) => {
return new Promise<void>((resolve) => {
for (let i = 0; i < numChunks; i++) {
onBytesReceived(Uint8Array.from([0xaa]))
}
resolve()
})
},
)
itemManager = {} as jest.Mocked<ItemManagerInterface>
itemManager.createItem = jest.fn()
@@ -52,6 +71,10 @@ describe('fileService', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn()
backupService = {} as jest.Mocked<BackupServiceInterface>
backupService.readEncryptedFileFromBackup = jest.fn()
backupService.getFileBackupInfo = jest.fn()
fileService = new FileService(
apiService,
itemManager,
@@ -61,6 +84,7 @@ describe('fileService', () => {
alertService,
crypto,
internalEventBus,
backupService,
)
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
@@ -110,6 +134,8 @@ describe('fileService', () => {
decryptedSize: 100_000,
} as jest.Mocked<FileItem>
apiService.downloadFile = jest.fn()
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
@@ -118,4 +144,42 @@ describe('fileService', () => {
expect(fileService['encryptedCache'].get(file.uuid)).toBeFalsy()
})
it('should download file from network if no backup', async () => {
const file = {
uuid: '1',
decryptedSize: 100_000,
encryptedSize: 101_000,
encryptedChunkSizes: [101_000],
} as jest.Mocked<FileItem>
backupService.getFileBackupInfo = jest.fn().mockReturnValue(undefined)
const downloadMock = apiService.downloadFile as jest.Mock
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
expect(downloadMock).toHaveBeenCalledTimes(1)
})
it('should download file from local backup if it exists', async () => {
const file = {
uuid: '1',
decryptedSize: 100_000,
encryptedSize: 101_000,
encryptedChunkSizes: [101_000],
} as jest.Mocked<FileItem>
backupService.getFileBackupInfo = jest.fn().mockReturnValue({})
const downloadMock = (apiService.downloadFile = jest.fn())
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
expect(downloadMock).toHaveBeenCalledTimes(0)
})
})

View File

@@ -19,7 +19,7 @@ import {
FileDecryptor,
FileDownloadProgress,
FilesClientInterface,
readAndDecryptBackupFile,
readAndDecryptBackupFileUsingFileSystemAPI,
FilesApiInterface,
FileBackupsConstantsV1,
FileBackupMetadataFile,
@@ -30,8 +30,9 @@ import {
DecryptedBytes,
OrderedByteChunker,
FileMemoryCache,
readAndDecryptBackupFileUsingBackupService,
BackupServiceInterface,
} from '@standardnotes/files'
import { AlertService } from '../Alert/AlertService'
import { ChallengeServiceInterface } from '../Challenge'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
@@ -39,6 +40,7 @@ import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
import { SyncServiceInterface } from '../Sync/SyncServiceInterface'
import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions'
import { log, LoggingDomain } from '../Logging'
const OneHundredMb = 100 * 1_000_000
@@ -54,6 +56,7 @@ export class FileService extends AbstractService implements FilesClientInterface
private alertService: AlertService,
private crypto: PureCryptoInterface,
protected override internalEventBus: InternalEventBusInterface,
private backupsService?: BackupServiceInterface,
) {
super(internalEventBus)
}
@@ -158,8 +161,8 @@ export class FileService extends AbstractService implements FilesClientInterface
let decryptedAggregate = new Uint8Array()
const orderedChunker = new OrderedByteChunker(file.encryptedChunkSizes, async (encryptedBytes) => {
const decryptedBytes = decryptOperation.decryptBytes(encryptedBytes)
const orderedChunker = new OrderedByteChunker(file.encryptedChunkSizes, 'memcache', async (chunk) => {
const decryptedBytes = decryptOperation.decryptBytes(chunk.data)
if (decryptedBytes) {
decryptedAggregate = new Uint8Array([...decryptedAggregate, ...decryptedBytes.decryptedBytes])
@@ -173,36 +176,60 @@ export class FileService extends AbstractService implements FilesClientInterface
public async downloadFile(
file: FileItem,
onDecryptedBytes: (decryptedBytes: Uint8Array, progress?: FileDownloadProgress) => Promise<void>,
onDecryptedBytes: (decryptedBytes: Uint8Array, progress: FileDownloadProgress) => Promise<void>,
): Promise<ClientDisplayableError | undefined> {
const cachedBytes = this.encryptedCache.get(file.uuid)
if (cachedBytes) {
const decryptedBytes = await this.decryptCachedEntry(file, cachedBytes)
await onDecryptedBytes(decryptedBytes.decryptedBytes, undefined)
await onDecryptedBytes(decryptedBytes.decryptedBytes, {
encryptedFileSize: cachedBytes.encryptedBytes.length,
encryptedBytesDownloaded: cachedBytes.encryptedBytes.length,
encryptedBytesRemaining: 0,
percentComplete: 100,
source: 'memcache',
})
return undefined
}
const addToCache = file.encryptedSize < this.encryptedCache.maxSize
const fileBackup = await this.backupsService?.getFileBackupInfo(file)
let cacheEntryAggregate = new Uint8Array()
if (this.backupsService && fileBackup) {
log(LoggingDomain.FilesService, 'Downloading file from backup', fileBackup)
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api)
await readAndDecryptBackupFileUsingBackupService(file, this.backupsService, this.crypto, async (chunk) => {
log(LoggingDomain.FilesService, 'Got local file chunk', chunk.progress)
const result = await operation.run(async ({ decrypted, encrypted, progress }): Promise<void> => {
if (addToCache) {
cacheEntryAggregate = new Uint8Array([...cacheEntryAggregate, ...encrypted.encryptedBytes])
return onDecryptedBytes(chunk.data, chunk.progress)
})
log(LoggingDomain.FilesService, 'Finished downloading file from backup')
return undefined
} else {
log(LoggingDomain.FilesService, 'Downloading file from network')
const addToCache = file.encryptedSize < this.encryptedCache.maxSize
let cacheEntryAggregate = new Uint8Array()
const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api)
const result = await operation.run(async ({ decrypted, encrypted, progress }): Promise<void> => {
if (addToCache) {
cacheEntryAggregate = new Uint8Array([...cacheEntryAggregate, ...encrypted.encryptedBytes])
}
return onDecryptedBytes(decrypted.decryptedBytes, progress)
})
if (addToCache && cacheEntryAggregate.byteLength > 0) {
this.encryptedCache.add(file.uuid, { encryptedBytes: cacheEntryAggregate })
}
return onDecryptedBytes(decrypted.decryptedBytes, progress)
})
if (addToCache) {
this.encryptedCache.add(file.uuid, { encryptedBytes: cacheEntryAggregate })
return result.error
}
return result.error
}
public async deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined> {
@@ -294,9 +321,15 @@ export class FileService extends AbstractService implements FilesClientInterface
return destinationFileHandle
}
const result = await readAndDecryptBackupFile(fileHandle, file, fileSystem, this.crypto, async (decryptedBytes) => {
await fileSystem.saveBytes(destinationFileHandle, decryptedBytes)
})
const result = await readAndDecryptBackupFileUsingFileSystemAPI(
fileHandle,
file,
fileSystem,
this.crypto,
async (decryptedBytes) => {
await fileSystem.saveBytes(destinationFileHandle, decryptedBytes)
},
)
await fileSystem.closeFileWriteStream(destinationFileHandle)
@@ -310,9 +343,15 @@ export class FileService extends AbstractService implements FilesClientInterface
): Promise<Uint8Array> {
let bytes = new Uint8Array()
await readAndDecryptBackupFile(fileHandle, file, fileSystem, this.crypto, async (decryptedBytes) => {
bytes = new Uint8Array([...bytes, ...decryptedBytes])
})
await readAndDecryptBackupFileUsingFileSystemAPI(
fileHandle,
file,
fileSystem,
this.crypto,
async (decryptedBytes) => {
bytes = new Uint8Array([...bytes, ...decryptedBytes])
},
)
return bytes
}