feat(files): refactor circular deps

This commit is contained in:
Karol Sójko
2022-08-04 16:21:46 +02:00
parent 7e251262d7
commit 696b82b9d3
33 changed files with 126 additions and 98 deletions

View File

@@ -0,0 +1,3 @@
export interface DirectoryHandle {
nativeHandle: unknown
}

View File

@@ -0,0 +1,3 @@
export interface FileHandleRead {
nativeHandle: unknown
}

View File

@@ -0,0 +1,4 @@
export interface FileHandleReadWrite {
nativeHandle: unknown
writableStream: unknown
}

View File

@@ -0,0 +1,19 @@
import { DirectoryHandle } from './DirectoryHandle'
import { FileHandleRead } from './FileHandleRead'
import { FileHandleReadWrite } from './FileHandleReadWrite'
import { FileSystemNoSelection } from './FileSystemNoSelection'
import { FileSystemResult } from './FileSystemResult'
export interface FileSystemApi {
selectDirectory(): Promise<DirectoryHandle | FileSystemNoSelection>
selectFile(): Promise<FileHandleRead | FileSystemNoSelection>
readFile(
file: FileHandleRead,
onBytes: (bytes: Uint8Array, isLast: boolean) => Promise<void>,
): Promise<FileSystemResult>
createDirectory(parentDirectory: DirectoryHandle, name: string): Promise<DirectoryHandle | FileSystemNoSelection>
createFile(directory: DirectoryHandle, name: string): Promise<FileHandleReadWrite | FileSystemNoSelection>
saveBytes(file: FileHandleReadWrite, bytes: Uint8Array): Promise<'success' | 'failed'>
saveString(file: FileHandleReadWrite, contents: string): Promise<'success' | 'failed'>
closeFileWriteStream(file: FileHandleReadWrite): Promise<'success' | 'failed'>
}

View File

@@ -0,0 +1 @@
export type FileSystemNoSelection = 'aborted' | 'failed'

View File

@@ -0,0 +1 @@
export type FileSystemResult = 'aborted' | 'success' | 'failed'

View File

@@ -0,0 +1,28 @@
import { StartUploadSessionResponse, MinimalHttpResponse, ClientDisplayableError } from '@standardnotes/responses'
import { FileContent } from '@standardnotes/models'
export interface FilesApiInterface {
startUploadSession(apiToken: string): Promise<StartUploadSessionResponse>
uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise<boolean>
closeUploadSession(apiToken: string): Promise<boolean>
downloadFile(
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] },
chunkIndex: number,
apiToken: string,
contentRangeStart: number,
onBytesReceived: (bytes: Uint8Array) => Promise<void>,
): Promise<ClientDisplayableError | undefined>
deleteFile(apiToken: string): Promise<MinimalHttpResponse>
createFileValetToken(
remoteIdentifier: string,
operation: 'write' | 'read' | 'delete',
unencryptedFileSize?: number,
): Promise<string | ClientDisplayableError>
getFilesDownloadUrl(): string
}

View File

@@ -1,173 +0,0 @@
import { ContentType, Uuid } from '@standardnotes/common'
import { EncryptionProvider } from '@standardnotes/encryption'
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import {
ItemManagerInterface,
FileBackupsDevice,
FileBackupsMapping,
AbstractService,
InternalEventBusInterface,
StatusServiceInterface,
FileBackupMetadataFile,
FilesApiInterface,
} from '@standardnotes/services'
export class FilesBackupService extends AbstractService {
private itemsObserverDisposer: () => void
private pendingFiles = new Set<Uuid>()
constructor(
private items: ItemManagerInterface,
private api: FilesApiInterface,
private encryptor: EncryptionProvider,
private device: FileBackupsDevice,
private status: StatusServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
this.itemsObserverDisposer = items.addObserver<FileItem>(ContentType.File, ({ changed, inserted, source }) => {
const applicableSources = [
PayloadEmitSource.LocalDatabaseLoaded,
PayloadEmitSource.RemoteSaved,
PayloadEmitSource.RemoteRetrieved,
]
if (applicableSources.includes(source)) {
void this.handleChangedFiles([...changed, ...inserted])
}
})
}
override deinit() {
this.itemsObserverDisposer()
}
public isFilesBackupsEnabled(): Promise<boolean> {
return this.device.isFilesBackupsEnabled()
}
public async enableFilesBackups(): Promise<void> {
await this.device.enableFilesBackups()
if (!(await this.isFilesBackupsEnabled())) {
return
}
this.backupAllFiles()
}
private backupAllFiles(): void {
const files = this.items.getItems<FileItem>(ContentType.File)
void this.handleChangedFiles(files)
}
public disableFilesBackups(): Promise<void> {
return this.device.disableFilesBackups()
}
public changeFilesBackupsLocation(): Promise<string | undefined> {
return this.device.changeFilesBackupsLocation()
}
public getFilesBackupsLocation(): Promise<string> {
return this.device.getFilesBackupsLocation()
}
public openFilesBackupsLocation(): Promise<void> {
return this.device.openFilesBackupsLocation()
}
private async getBackupsMapping(): Promise<FileBackupsMapping['files']> {
return (await this.device.getFilesBackupsMappingFile()).files
}
private async handleChangedFiles(files: FileItem[]): Promise<void> {
if (files.length === 0) {
return
}
if (!(await this.isFilesBackupsEnabled())) {
return
}
const mapping = await this.getBackupsMapping()
for (const file of files) {
if (this.pendingFiles.has(file.uuid)) {
continue
}
const record = mapping[file.uuid]
if (record == undefined) {
this.pendingFiles.add(file.uuid)
await this.performBackupOperation(file)
this.pendingFiles.delete(file.uuid)
}
}
}
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
const messageId = this.status.addMessage(`Backing up file ${file.name}...`)
const encryptedFile = await this.encryptor.encryptSplitSingle({
usesItemsKeyWithKeyLookup: {
items: [file.payload],
},
})
const itemsKey = this.items.getDisplayableItemsKeys().find((k) => k.uuid === encryptedFile.items_key_id)
if (!itemsKey) {
return 'failed'
}
const encryptedItemsKey = await this.encryptor.encryptSplitSingle({
usesRootKeyWithKeyLookup: {
items: [itemsKey.payload],
},
})
const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read')
if (token instanceof ClientDisplayableError) {
return 'failed'
}
const metaFile: FileBackupMetadataFile = {
info: {
warning: 'Do not edit this file.',
information: 'The file and key data below is encrypted with your account password.',
instructions:
'Drag and drop this metadata file into the File Backups preferences pane in the Standard Notes desktop or web application interface.',
},
file: CreateEncryptedBackupFileContextPayload(encryptedFile.ejected()),
itemsKey: CreateEncryptedBackupFileContextPayload(encryptedItemsKey.ejected()),
version: '1.0.0',
}
const metaFileAsString = JSON.stringify(metaFile, null, 2)
const result = await this.device.saveFilesBackupsFile(file.uuid, metaFileAsString, {
chunkSizes: file.encryptedChunkSizes,
url: this.api.getFilesDownloadUrl(),
valetToken: token,
})
this.status.removeMessage(messageId)
if (result === 'failed') {
const failMessageId = this.status.addMessage(`Failed to back up ${file.name}...`)
setTimeout(() => {
this.status.removeMessage(failMessageId)
}, 2000)
}
return result
}
}

View File

@@ -0,0 +1,8 @@
import { BackupFileEncryptedContextualPayload } from '@standardnotes/models'
export interface FileBackupMetadataFile {
info: Record<string, string>
file: BackupFileEncryptedContextualPayload
itemsKey: BackupFileEncryptedContextualPayload
version: '1.0.0'
}

View File

@@ -0,0 +1,5 @@
export const FileBackupsConstantsV1 = {
Version: '1.0.0',
MetadataFileName: 'metadata.sn.json',
BinaryFileName: 'file.encrypted',
}

View File

@@ -0,0 +1,21 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsMapping } from './FileBackupsMapping'
export interface FileBackupsDevice {
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
saveFilesBackupsFile(
uuid: Uuid,
metaFile: string,
downloadRequest: {
chunkSizes: number[]
valetToken: string
url: string
},
): Promise<'success' | 'failed'>
isFilesBackupsEnabled(): Promise<boolean>
enableFilesBackups(): Promise<void>
disableFilesBackups(): Promise<void>
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
openFilesBackupsLocation(): Promise<void>
}

View File

@@ -0,0 +1,17 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
export interface FileBackupsMapping {
version: typeof FileBackupsConstantsV1.Version
files: Record<
Uuid,
{
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
version: typeof FileBackupsConstantsV1.Version
}
>
}

View File

@@ -1,9 +1,9 @@
import { sleep } from '@standardnotes/utils'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FilesApiInterface } from '@standardnotes/services'
import { DownloadAndDecryptFileOperation } from './DownloadAndDecrypt'
import { FileContent } from '@standardnotes/models'
import { FilesApiInterface } from '../Api/FilesApiInterface'
describe('download and decrypt', () => {
let apiService: FilesApiInterface

View File

@@ -2,10 +2,10 @@ import { ClientDisplayableError } from '@standardnotes/responses'
import { AbortFunction, FileDownloader } from '../UseCase/FileDownloader'
import { FileDecryptor } from '../UseCase/FileDecryptor'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FilesApiInterface } from '@standardnotes/services'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FileContent } from '@standardnotes/models'
import { DecryptedBytes, EncryptedBytes } from '@standardnotes/filepicker'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export type DownloadAndDecryptResult = { success: boolean; error?: ClientDisplayableError; aborted?: boolean }

View File

@@ -1,8 +1,9 @@
import { EncryptAndUploadFileOperation } from './EncryptAndUpload'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { FilesApiInterface } from '@standardnotes/services'
import { FileContent } from '@standardnotes/models'
import { FilesApiInterface } from '../Api/FilesApiInterface'
describe('encrypt and upload', () => {
let apiService: FilesApiInterface
let operation: EncryptAndUploadFileOperation

View File

@@ -1,10 +1,10 @@
import { FileUploadProgress } from '../Types/FileUploadProgress'
import { FileUploadResult } from '../Types/FileUploadResult'
import { FilesApiInterface } from '@standardnotes/services'
import { FileUploader } from '../UseCase/FileUploader'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { FileEncryptor } from '../UseCase/FileEncryptor'
import { FileContent } from '@standardnotes/models'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export class EncryptAndUploadFileOperation {
public readonly encryptedChunkSizes: number[] = []

View File

@@ -1,121 +0,0 @@
import {
InternalEventBusInterface,
SyncServiceInterface,
ItemManagerInterface,
AlertService,
ApiServiceInterface,
ChallengeServiceInterface,
} from '@standardnotes/services'
import { FileService } from './FileService'
import { PureCryptoInterface, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { FileItem } from '@standardnotes/models'
import { EncryptionProvider } from '@standardnotes/encryption'
describe('fileService', () => {
let apiService: ApiServiceInterface
let itemManager: ItemManagerInterface
let syncService: SyncServiceInterface
let alertService: AlertService
let crypto: PureCryptoInterface
let challengor: ChallengeServiceInterface
let fileService: FileService
let encryptor: EncryptionProvider
let internalEventBus: InternalEventBusInterface
beforeEach(() => {
apiService = {} as jest.Mocked<ApiServiceInterface>
apiService.addEventObserver = jest.fn()
apiService.createFileValetToken = jest.fn()
apiService.downloadFile = jest.fn()
apiService.deleteFile = jest.fn().mockReturnValue({})
itemManager = {} as jest.Mocked<ItemManagerInterface>
itemManager.createItem = jest.fn()
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
itemManager.setItemToBeDeleted = jest.fn()
itemManager.addObserver = jest.fn()
itemManager.changeItem = jest.fn()
challengor = {} as jest.Mocked<ChallengeServiceInterface>
syncService = {} as jest.Mocked<SyncServiceInterface>
syncService.sync = jest.fn()
encryptor = {} as jest.Mocked<EncryptionProvider>
alertService = {} as jest.Mocked<AlertService>
alertService.confirm = jest.fn().mockReturnValue(true)
alertService.alert = jest.fn()
crypto = {} as jest.Mocked<PureCryptoInterface>
crypto.base64Decode = jest.fn()
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn()
fileService = new FileService(
apiService,
itemManager,
syncService,
encryptor,
challengor,
alertService,
crypto,
internalEventBus,
)
crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
state: {},
} as StreamEncryptor)
crypto.xchacha20StreamDecryptorPush = jest.fn().mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 })
crypto.xchacha20StreamInitEncryptor = jest.fn().mockReturnValue({
header: 'some-header',
state: {},
} as StreamEncryptor)
crypto.xchacha20StreamEncryptorPush = jest.fn().mockReturnValue(new Uint8Array())
})
it.only('should cache file after download', async () => {
const file = {
uuid: '1',
decryptedSize: 100_000,
encryptedSize: 101_000,
encryptedChunkSizes: [101_000],
} as jest.Mocked<FileItem>
let downloadMock = apiService.downloadFile as jest.Mock
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
expect(downloadMock).toHaveBeenCalledTimes(1)
downloadMock = apiService.downloadFile = jest.fn()
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
expect(downloadMock).toHaveBeenCalledTimes(0)
expect(fileService['encryptedCache'].get(file.uuid)).toBeTruthy()
})
it('deleting file should remove it from cache', async () => {
const file = {
uuid: '1',
decryptedSize: 100_000,
} as jest.Mocked<FileItem>
await fileService.downloadFile(file, async () => {
return Promise.resolve()
})
await fileService.deleteFile(file)
expect(fileService['encryptedCache'].get(file.uuid)).toBeFalsy()
})
})

View File

@@ -1,314 +0,0 @@
import { DecryptedBytes, EncryptedBytes, FileMemoryCache, OrderedByteChunker } from '@standardnotes/filepicker'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
import { DownloadAndDecryptFileOperation } from '../Operations/DownloadAndDecrypt'
import { EncryptAndUploadFileOperation } from '../Operations/EncryptAndUpload'
import {
FileItem,
FileProtocolV1Constants,
FileMetadata,
FileContentSpecialized,
FillItemContentSpecialized,
FileContent,
EncryptedPayload,
isEncryptedPayload,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { UuidGenerator } from '@standardnotes/utils'
import {
AbstractService,
InternalEventBusInterface,
ItemManagerInterface,
SyncServiceInterface,
AlertService,
FileSystemApi,
FilesApiInterface,
FileBackupMetadataFile,
FileHandleRead,
FileSystemNoSelection,
ChallengeServiceInterface,
FileBackupsConstantsV1,
} from '@standardnotes/services'
import { FilesClientInterface } from './FilesClientInterface'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { readAndDecryptBackupFile } from './ReadAndDecryptBackupFile'
import { DecryptItemsKeyWithUserFallback, EncryptionProvider, SNItemsKey } from '@standardnotes/encryption'
import { FileDecryptor } from '../UseCase/FileDecryptor'
const OneHundredMb = 100 * 1_000_000
export class FileService extends AbstractService implements FilesClientInterface {
private encryptedCache: FileMemoryCache = new FileMemoryCache(OneHundredMb)
constructor(
private api: FilesApiInterface,
private itemManager: ItemManagerInterface,
private syncService: SyncServiceInterface,
private encryptor: EncryptionProvider,
private challengor: ChallengeServiceInterface,
private alertService: AlertService,
private crypto: PureCryptoInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
}
override deinit(): void {
super.deinit()
this.encryptedCache.clear()
;(this.encryptedCache as unknown) = undefined
;(this.api as unknown) = undefined
;(this.itemManager as unknown) = undefined
;(this.encryptor as unknown) = undefined
;(this.syncService as unknown) = undefined
;(this.alertService as unknown) = undefined
;(this.challengor as unknown) = undefined
;(this.crypto as unknown) = undefined
}
public minimumChunkSize(): number {
return 5_000_000
}
public async beginNewFileUpload(
sizeInBytes: number,
): Promise<EncryptAndUploadFileOperation | ClientDisplayableError> {
const remoteIdentifier = UuidGenerator.GenerateUuid()
const tokenResult = await this.api.createFileValetToken(remoteIdentifier, 'write', sizeInBytes)
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}
const key = this.crypto.generateRandomKey(FileProtocolV1Constants.KeySize)
const fileParams = {
key,
remoteIdentifier,
decryptedSize: sizeInBytes,
}
const uploadOperation = new EncryptAndUploadFileOperation(fileParams, tokenResult, this.crypto, this.api)
const uploadSessionStarted = await this.api.startUploadSession(tokenResult)
if (!uploadSessionStarted.uploadId) {
return new ClientDisplayableError('Could not start upload session')
}
return uploadOperation
}
public async pushBytesForUpload(
operation: EncryptAndUploadFileOperation,
bytes: Uint8Array,
chunkId: number,
isFinalChunk: boolean,
): Promise<ClientDisplayableError | undefined> {
const success = await operation.pushBytes(bytes, chunkId, isFinalChunk)
if (!success) {
return new ClientDisplayableError('Failed to push file bytes to server')
}
return undefined
}
public async finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
): Promise<FileItem | ClientDisplayableError> {
const uploadSessionClosed = await this.api.closeUploadSession(operation.getApiToken())
if (!uploadSessionClosed) {
return new ClientDisplayableError('Could not close upload session')
}
const result = operation.getResult()
const fileContent: FileContentSpecialized = {
decryptedSize: result.finalDecryptedSize,
encryptedChunkSizes: operation.encryptedChunkSizes,
encryptionHeader: result.encryptionHeader,
key: result.key,
mimeType: fileMetadata.mimeType,
name: fileMetadata.name,
remoteIdentifier: result.remoteIdentifier,
}
const file = await this.itemManager.createItem<FileItem>(
ContentType.File,
FillItemContentSpecialized(fileContent),
true,
)
await this.syncService.sync()
return file
}
private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
const decryptOperation = new FileDecryptor(file, this.crypto)
let decryptedAggregate = new Uint8Array()
const orderedChunker = new OrderedByteChunker(file.encryptedChunkSizes, async (encryptedBytes) => {
const decryptedBytes = decryptOperation.decryptBytes(encryptedBytes)
if (decryptedBytes) {
decryptedAggregate = new Uint8Array([...decryptedAggregate, ...decryptedBytes.decryptedBytes])
}
})
await orderedChunker.addBytes(entry.encryptedBytes)
return { decryptedBytes: decryptedAggregate }
}
public async downloadFile(
file: FileItem,
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)
return undefined
}
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) {
this.encryptedCache.add(file.uuid, { encryptedBytes: cacheEntryAggregate })
}
return result.error
}
public async deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined> {
this.encryptedCache.remove(file.uuid)
const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'delete')
if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}
const result = await this.api.deleteFile(tokenResult)
if (result.error) {
return ClientDisplayableError.FromError(result.error)
}
await this.itemManager.setItemToBeDeleted(file)
await this.syncService.sync()
return undefined
}
public isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false {
if (name === FileBackupsConstantsV1.MetadataFileName) {
return 'metadata'
} else if (name === FileBackupsConstantsV1.BinaryFileName) {
return 'binary'
}
return false
}
public async decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise<FileItem | undefined> {
const encryptedItemsKey = new EncryptedPayload({
...metdataFile.itemsKey,
waitingForKey: false,
errorDecrypting: false,
})
const decryptedItemsKeyResult = await DecryptItemsKeyWithUserFallback(
encryptedItemsKey,
this.encryptor,
this.challengor,
)
if (decryptedItemsKeyResult === 'failed' || decryptedItemsKeyResult === 'aborted') {
return undefined
}
const encryptedFile = new EncryptedPayload({ ...metdataFile.file, waitingForKey: false, errorDecrypting: false })
const itemsKey = new SNItemsKey(decryptedItemsKeyResult)
const decryptedFile = await this.encryptor.decryptSplitSingle<FileContent>({
usesItemsKey: {
items: [encryptedFile],
key: itemsKey,
},
})
if (isEncryptedPayload(decryptedFile)) {
return undefined
}
return new FileItem(decryptedFile)
}
public async selectFile(fileSystem: FileSystemApi): Promise<FileHandleRead | FileSystemNoSelection> {
const result = await fileSystem.selectFile()
return result
}
public async readBackupFileAndSaveDecrypted(
fileHandle: FileHandleRead,
file: FileItem,
fileSystem: FileSystemApi,
): Promise<'success' | 'aborted' | 'failed'> {
const destinationDirectoryHandle = await fileSystem.selectDirectory()
if (destinationDirectoryHandle === 'aborted' || destinationDirectoryHandle === 'failed') {
return destinationDirectoryHandle
}
const destinationFileHandle = await fileSystem.createFile(destinationDirectoryHandle, file.name)
if (destinationFileHandle === 'aborted' || destinationFileHandle === 'failed') {
return destinationFileHandle
}
const result = await readAndDecryptBackupFile(fileHandle, file, fileSystem, this.crypto, async (decryptedBytes) => {
await fileSystem.saveBytes(destinationFileHandle, decryptedBytes)
})
await fileSystem.closeFileWriteStream(destinationFileHandle)
return result
}
public async readBackupFileBytesDecrypted(
fileHandle: FileHandleRead,
file: FileItem,
fileSystem: FileSystemApi,
): Promise<Uint8Array> {
let bytes = new Uint8Array()
await readAndDecryptBackupFile(fileHandle, file, fileSystem, this.crypto, async (decryptedBytes) => {
bytes = new Uint8Array([...bytes, ...decryptedBytes])
})
return bytes
}
}

View File

@@ -2,7 +2,10 @@ import { EncryptAndUploadFileOperation } from '../Operations/EncryptAndUpload'
import { FileItem, FileMetadata } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FileSystemApi, FileBackupMetadataFile, FileHandleRead, FileSystemNoSelection } from '@standardnotes/services'
import { FileSystemApi } from '../Api/FileSystemApi'
import { FileHandleRead } from '../Api/FileHandleRead'
import { FileSystemNoSelection } from '../Api/FileSystemNoSelection'
import { FileBackupMetadataFile } from '../Device/FileBackupMetadataFile'
export interface FilesClientInterface {
beginNewFileUpload(sizeInBytes: number): Promise<EncryptAndUploadFileOperation | ClientDisplayableError>

View File

@@ -1,8 +1,9 @@
import { FileContent } from '@standardnotes/models'
import { FileSystemApi, FileHandleRead } from '@standardnotes/services'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { OrderedByteChunker } from '@standardnotes/filepicker'
import { FileDecryptor } from '../UseCase/FileDecryptor'
import { FileSystemApi } from '../Api/FileSystemApi'
import { FileHandleRead } from '../Api/FileHandleRead'
export async function readAndDecryptBackupFile(
fileHandle: FileHandleRead,

View File

@@ -1,5 +1,5 @@
import { FileContent } from '@standardnotes/models'
import { FilesApiInterface } from '@standardnotes/services'
import { FilesApiInterface } from '../Api/FilesApiInterface'
import { FileDownloader } from './FileDownloader'
describe('file downloader', () => {

View File

@@ -1,8 +1,8 @@
import { ClientDisplayableError } from '@standardnotes/responses'
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
import { FilesApiInterface } from '@standardnotes/services'
import { Deferred } from '@standardnotes/utils'
import { FileContent } from '@standardnotes/models'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export type AbortSignal = 'aborted'
export type AbortFunction = () => void

View File

@@ -1,4 +1,4 @@
import { FilesApiInterface } from '@standardnotes/services'
import { FilesApiInterface } from '../Api/FilesApiInterface'
import { FileUploader } from './FileUploader'
describe('file uploader', () => {

View File

@@ -1,4 +1,4 @@
import { FilesApiInterface } from '@standardnotes/services'
import { FilesApiInterface } from '../Api/FilesApiInterface'
export class FileUploader {
constructor(private apiService: FilesApiInterface) {}

View File

@@ -1,5 +1,16 @@
export * from './Service/FileService'
export * from './Api/DirectoryHandle'
export * from './Api/FileHandleRead'
export * from './Api/FileHandleReadWrite'
export * from './Api/FileSystemApi'
export * from './Api/FileSystemNoSelection'
export * from './Api/FileSystemResult'
export * from './Api/FilesApiInterface'
export * from './Device/FileBackupMetadataFile'
export * from './Device/FileBackupsConstantsV1'
export * from './Device/FileBackupsDevice'
export * from './Device/FileBackupsMapping'
export * from './Service/FilesClientInterface'
export * from './Service/ReadAndDecryptBackupFile'
export * from './Operations/DownloadAndDecrypt'
export * from './Operations/EncryptAndUpload'
export * from './UseCase/FileDecryptor'
@@ -9,4 +20,3 @@ export * from './UseCase/FileDownloader'
export * from './Types/FileDownloadProgress'
export * from './Types/FileUploadProgress'
export * from './Types/FileUploadResult'
export * from './Backups/BackupService'