internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
11
packages/files/src/Domain/Api/DownloadFileParams.ts
Normal file
11
packages/files/src/Domain/Api/DownloadFileParams.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FileContent } from '@standardnotes/models'
|
||||
import { FileOwnershipType } from './FileOwnershipType'
|
||||
|
||||
export type DownloadFileParams = {
|
||||
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] }
|
||||
chunkIndex: number
|
||||
valetToken: string
|
||||
ownershipType: FileOwnershipType
|
||||
contentRangeStart: number
|
||||
onBytesReceived: (bytes: Uint8Array) => Promise<void>
|
||||
}
|
||||
1
packages/files/src/Domain/Api/FileOwnershipType.ts
Normal file
1
packages/files/src/Domain/Api/FileOwnershipType.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type FileOwnershipType = 'user' | 'shared-vault'
|
||||
@@ -1,28 +1,38 @@
|
||||
import { StartUploadSessionResponse, HttpResponse, ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { FileContent } from '@standardnotes/models'
|
||||
import {
|
||||
StartUploadSessionResponse,
|
||||
HttpResponse,
|
||||
ClientDisplayableError,
|
||||
ValetTokenOperation,
|
||||
} from '@standardnotes/responses'
|
||||
import { DownloadFileParams } from './DownloadFileParams'
|
||||
import { FileOwnershipType } from './FileOwnershipType'
|
||||
|
||||
export interface FilesApiInterface {
|
||||
startUploadSession(apiToken: string): Promise<HttpResponse<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<HttpResponse>
|
||||
|
||||
createFileValetToken(
|
||||
createUserFileValetToken(
|
||||
remoteIdentifier: string,
|
||||
operation: 'write' | 'read' | 'delete',
|
||||
operation: ValetTokenOperation,
|
||||
unencryptedFileSize?: number,
|
||||
): Promise<string | ClientDisplayableError>
|
||||
|
||||
getFilesDownloadUrl(): string
|
||||
startUploadSession(
|
||||
valetToken: string,
|
||||
ownershipType: FileOwnershipType,
|
||||
): Promise<HttpResponse<StartUploadSessionResponse>>
|
||||
|
||||
uploadFileBytes(
|
||||
valetToken: string,
|
||||
ownershipType: FileOwnershipType,
|
||||
chunkId: number,
|
||||
encryptedBytes: Uint8Array,
|
||||
): Promise<boolean>
|
||||
|
||||
closeUploadSession(valetToken: string, ownershipType: FileOwnershipType): Promise<boolean>
|
||||
|
||||
downloadFile(params: DownloadFileParams): Promise<ClientDisplayableError | undefined>
|
||||
|
||||
moveFile(valetToken: string): Promise<boolean>
|
||||
|
||||
deleteFile(valetToken: string, ownershipType: FileOwnershipType): Promise<HttpResponse>
|
||||
|
||||
getFilesDownloadUrl(ownershipType: FileOwnershipType): string
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ describe('download and decrypt', () => {
|
||||
let apiService: FilesApiInterface
|
||||
let operation: DownloadAndDecryptFileOperation
|
||||
let file: {
|
||||
uuid: string
|
||||
encryptedChunkSizes: FileContent['encryptedChunkSizes']
|
||||
encryptionHeader: FileContent['encryptionHeader']
|
||||
remoteIdentifier: FileContent['remoteIdentifier']
|
||||
key: FileContent['key']
|
||||
shared_vault_uuid: string | undefined
|
||||
}
|
||||
let crypto: PureCryptoInterface
|
||||
|
||||
@@ -26,16 +28,16 @@ describe('download and decrypt', () => {
|
||||
apiService.downloadFile = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
_file: string,
|
||||
_chunkIndex: number,
|
||||
_apiToken: string,
|
||||
_rangeStart: number,
|
||||
onBytesReceived: (bytes: Uint8Array) => void,
|
||||
) => {
|
||||
(params: {
|
||||
_file: string
|
||||
_chunkIndex: number
|
||||
_apiToken: string
|
||||
_rangeStart: number
|
||||
onBytesReceived: (bytes: Uint8Array) => void
|
||||
}) => {
|
||||
const receiveFile = async () => {
|
||||
for (let i = 0; i < NumChunks; i++) {
|
||||
onBytesReceived(chunkOfSize(size))
|
||||
params.onBytesReceived(chunkOfSize(size))
|
||||
|
||||
await sleep(100, false)
|
||||
}
|
||||
@@ -50,7 +52,7 @@ describe('download and decrypt', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<FilesApiInterface>
|
||||
apiService.createFileValetToken = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
downloadChunksOfSize(5)
|
||||
|
||||
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||
@@ -62,17 +64,19 @@ describe('download and decrypt', () => {
|
||||
crypto.xchacha20StreamDecryptorPush = jest.fn().mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 })
|
||||
|
||||
file = {
|
||||
uuid: '123',
|
||||
encryptedChunkSizes: [100_000],
|
||||
remoteIdentifier: '123',
|
||||
key: 'secret',
|
||||
encryptionHeader: 'some-header',
|
||||
shared_vault_uuid: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
it('run should resolve when operation is complete', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService)
|
||||
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own')
|
||||
|
||||
await operation.run(async (result) => {
|
||||
if (result) {
|
||||
@@ -87,15 +91,17 @@ describe('download and decrypt', () => {
|
||||
|
||||
it('should correctly report progress', async () => {
|
||||
file = {
|
||||
uuid: '123',
|
||||
encryptedChunkSizes: [100_000, 200_000, 200_000],
|
||||
remoteIdentifier: '123',
|
||||
key: 'secret',
|
||||
encryptionHeader: 'some-header',
|
||||
shared_vault_uuid: undefined,
|
||||
}
|
||||
|
||||
downloadChunksOfSize(100_000)
|
||||
|
||||
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService)
|
||||
operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own')
|
||||
|
||||
const progress: FileDownloadProgress = await new Promise((resolve) => {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
|
||||
@@ -21,6 +21,8 @@ export class DownloadAndDecryptFileOperation {
|
||||
|
||||
constructor(
|
||||
private readonly file: {
|
||||
uuid: string
|
||||
shared_vault_uuid: string | undefined
|
||||
encryptedChunkSizes: FileContent['encryptedChunkSizes']
|
||||
encryptionHeader: FileContent['encryptionHeader']
|
||||
remoteIdentifier: FileContent['remoteIdentifier']
|
||||
@@ -28,8 +30,9 @@ export class DownloadAndDecryptFileOperation {
|
||||
},
|
||||
private readonly crypto: PureCryptoInterface,
|
||||
private readonly api: FilesApiInterface,
|
||||
valetToken: string,
|
||||
) {
|
||||
this.downloader = new FileDownloader(this.file, this.api)
|
||||
this.downloader = new FileDownloader(this.file, this.api, valetToken)
|
||||
}
|
||||
|
||||
private createDecryptor(): FileDecryptor {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FileUploadResult } from '../Types/FileUploadResult'
|
||||
import { FileUploader } from '../UseCase/FileUploader'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { FileEncryptor } from '../UseCase/FileEncryptor'
|
||||
import { FileContent } from '@standardnotes/models'
|
||||
import { FileContent, VaultListingInterface } from '@standardnotes/models'
|
||||
import { FilesApiInterface } from '../Api/FilesApiInterface'
|
||||
|
||||
export class EncryptAndUploadFileOperation {
|
||||
@@ -22,9 +22,10 @@ export class EncryptAndUploadFileOperation {
|
||||
key: FileContent['key']
|
||||
remoteIdentifier: FileContent['remoteIdentifier']
|
||||
},
|
||||
private apiToken: string,
|
||||
private valetToken: string,
|
||||
private crypto: PureCryptoInterface,
|
||||
private api: FilesApiInterface,
|
||||
public readonly vault?: VaultListingInterface,
|
||||
) {
|
||||
this.encryptor = new FileEncryptor(file, this.crypto)
|
||||
this.uploader = new FileUploader(this.api)
|
||||
@@ -32,8 +33,8 @@ export class EncryptAndUploadFileOperation {
|
||||
this.encryptionHeader = this.encryptor.initializeHeader()
|
||||
}
|
||||
|
||||
public getApiToken(): string {
|
||||
return this.apiToken
|
||||
public getValetToken(): string {
|
||||
return this.valetToken
|
||||
}
|
||||
|
||||
public getProgress(): FileUploadProgress {
|
||||
@@ -79,7 +80,12 @@ export class EncryptAndUploadFileOperation {
|
||||
}
|
||||
|
||||
private async uploadBytes(encryptedBytes: Uint8Array, chunkId: number): Promise<boolean> {
|
||||
const success = await this.uploader.uploadBytes(encryptedBytes, chunkId, this.apiToken)
|
||||
const success = await this.uploader.uploadBytes(
|
||||
encryptedBytes,
|
||||
this.vault && this.vault.sharing ? 'shared-vault' : 'user',
|
||||
chunkId,
|
||||
this.valetToken,
|
||||
)
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EncryptAndUploadFileOperation } from '../Operations/EncryptAndUpload'
|
||||
import { FileItem, FileMetadata } from '@standardnotes/models'
|
||||
import { FileItem, FileMetadata, VaultListingInterface, SharedVaultListingInterface } from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { FileDownloadProgress } from '../Types/FileDownloadProgress'
|
||||
import { FileSystemApi } from '../Api/FileSystemApi'
|
||||
@@ -8,15 +8,18 @@ import { FileSystemNoSelection } from '../Api/FileSystemNoSelection'
|
||||
import { FileBackupMetadataFile } from '../Device/FileBackupMetadataFile'
|
||||
|
||||
export interface FilesClientInterface {
|
||||
beginNewFileUpload(sizeInBytes: number): Promise<EncryptAndUploadFileOperation | ClientDisplayableError>
|
||||
minimumChunkSize(): number
|
||||
|
||||
beginNewFileUpload(
|
||||
sizeInBytes: number,
|
||||
vault?: VaultListingInterface,
|
||||
): Promise<EncryptAndUploadFileOperation | ClientDisplayableError>
|
||||
pushBytesForUpload(
|
||||
operation: EncryptAndUploadFileOperation,
|
||||
bytes: Uint8Array,
|
||||
chunkId: number,
|
||||
isFinalChunk: boolean,
|
||||
): Promise<ClientDisplayableError | undefined>
|
||||
|
||||
finishUpload(
|
||||
operation: EncryptAndUploadFileOperation,
|
||||
fileMetadata: FileMetadata,
|
||||
@@ -29,20 +32,21 @@ export interface FilesClientInterface {
|
||||
|
||||
deleteFile(file: FileItem): Promise<ClientDisplayableError | undefined>
|
||||
|
||||
minimumChunkSize(): number
|
||||
|
||||
isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false
|
||||
|
||||
decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise<FileItem | undefined>
|
||||
moveFileToSharedVault(
|
||||
file: FileItem,
|
||||
sharedVault: SharedVaultListingInterface,
|
||||
): Promise<void | ClientDisplayableError>
|
||||
moveFileOutOfSharedVault(file: FileItem): Promise<void | ClientDisplayableError>
|
||||
|
||||
selectFile(fileSystem: FileSystemApi): Promise<FileHandleRead | FileSystemNoSelection>
|
||||
|
||||
isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false
|
||||
decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise<FileItem | undefined>
|
||||
readBackupFileAndSaveDecrypted(
|
||||
fileHandle: FileHandleRead,
|
||||
file: FileItem,
|
||||
fileSystem: FileSystemApi,
|
||||
): Promise<'success' | 'aborted' | 'failed'>
|
||||
|
||||
readBackupFileBytesDecrypted(
|
||||
fileHandle: FileHandleRead,
|
||||
file: FileItem,
|
||||
|
||||
@@ -6,6 +6,8 @@ describe('file downloader', () => {
|
||||
let apiService: FilesApiInterface
|
||||
let downloader: FileDownloader
|
||||
let file: {
|
||||
uuid: string
|
||||
shared_vault_uuid: string | undefined
|
||||
encryptedChunkSizes: FileContent['encryptedChunkSizes']
|
||||
remoteIdentifier: FileContent['remoteIdentifier']
|
||||
}
|
||||
@@ -14,20 +16,20 @@ describe('file downloader', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = {} as jest.Mocked<FilesApiInterface>
|
||||
apiService.createFileValetToken = jest.fn()
|
||||
apiService.createUserFileValetToken = jest.fn()
|
||||
apiService.downloadFile = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
_file: string,
|
||||
_chunkIndex: number,
|
||||
_apiToken: string,
|
||||
_rangeStart: number,
|
||||
onBytesReceived: (bytes: Uint8Array) => void,
|
||||
) => {
|
||||
(params: {
|
||||
_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]))
|
||||
params.onBytesReceived(Uint8Array.from([0xaa]))
|
||||
}
|
||||
|
||||
resolve()
|
||||
@@ -36,6 +38,8 @@ describe('file downloader', () => {
|
||||
)
|
||||
|
||||
file = {
|
||||
uuid: '123',
|
||||
shared_vault_uuid: undefined,
|
||||
encryptedChunkSizes: [100_000],
|
||||
remoteIdentifier: '123',
|
||||
}
|
||||
@@ -44,7 +48,7 @@ describe('file downloader', () => {
|
||||
it('should pass back bytes as they are received', async () => {
|
||||
let receivedBytes = new Uint8Array()
|
||||
|
||||
downloader = new FileDownloader(file, apiService)
|
||||
downloader = new FileDownloader(file, apiService, 'valet-token')
|
||||
|
||||
expect(receivedBytes.length).toBe(0)
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ export class FileDownloader {
|
||||
|
||||
constructor(
|
||||
private file: {
|
||||
uuid: string
|
||||
shared_vault_uuid: string | undefined
|
||||
encryptedChunkSizes: FileContent['encryptedChunkSizes']
|
||||
remoteIdentifier: FileContent['remoteIdentifier']
|
||||
},
|
||||
private readonly api: FilesApiInterface,
|
||||
private readonly valetToken: string,
|
||||
) {}
|
||||
|
||||
private getProgress(): FileDownloadProgress {
|
||||
@@ -40,22 +43,10 @@ export class FileDownloader {
|
||||
}
|
||||
|
||||
public async run(onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
|
||||
const tokenResult = await this.getValetToken()
|
||||
|
||||
if (tokenResult instanceof ClientDisplayableError) {
|
||||
return tokenResult
|
||||
}
|
||||
|
||||
return this.performDownload(tokenResult, onEncryptedBytes)
|
||||
return this.performDownload(onEncryptedBytes)
|
||||
}
|
||||
|
||||
private async getValetToken(): Promise<string | ClientDisplayableError> {
|
||||
const tokenResult = await this.api.createFileValetToken(this.file.remoteIdentifier, 'read')
|
||||
|
||||
return tokenResult
|
||||
}
|
||||
|
||||
private async performDownload(valetToken: string, onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
|
||||
private async performDownload(onEncryptedBytes: OnEncryptedBytes): Promise<FileDownloaderResult> {
|
||||
const chunkIndex = 0
|
||||
const startRange = 0
|
||||
|
||||
@@ -69,7 +60,14 @@ export class FileDownloader {
|
||||
await onEncryptedBytes(bytes, this.getProgress(), this.abort)
|
||||
}
|
||||
|
||||
const downloadPromise = this.api.downloadFile(this.file, chunkIndex, valetToken, startRange, onRemoteBytesReceived)
|
||||
const downloadPromise = this.api.downloadFile({
|
||||
file: this.file,
|
||||
chunkIndex,
|
||||
valetToken: this.valetToken,
|
||||
contentRangeStart: startRange,
|
||||
onBytesReceived: onRemoteBytesReceived,
|
||||
ownershipType: this.file.shared_vault_uuid ? 'shared-vault' : 'user',
|
||||
})
|
||||
|
||||
const result = await Promise.race([this.abortDeferred.promise, downloadPromise])
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FilesApiInterface } from '../Api/FilesApiInterface'
|
||||
import { FileUploader } from './FileUploader'
|
||||
|
||||
describe('file uploader', () => {
|
||||
let apiService
|
||||
let apiService: FilesApiInterface
|
||||
let uploader: FileUploader
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -14,7 +14,7 @@ describe('file uploader', () => {
|
||||
|
||||
it('should return true when a chunk is uploaded', async () => {
|
||||
const bytes = new Uint8Array()
|
||||
const success = await uploader.uploadBytes(bytes, 2, 'api-token')
|
||||
const success = await uploader.uploadBytes(bytes, 'user', 2, 'api-token')
|
||||
|
||||
expect(success).toEqual(true)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { FileOwnershipType } from '../Api/FileOwnershipType'
|
||||
import { FilesApiInterface } from '../Api/FilesApiInterface'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(private apiService: FilesApiInterface) {}
|
||||
|
||||
public async uploadBytes(encryptedBytes: Uint8Array, chunkId: number, apiToken: string): Promise<boolean> {
|
||||
const result = await this.apiService.uploadFileBytes(apiToken, chunkId, encryptedBytes)
|
||||
public async uploadBytes(
|
||||
encryptedBytes: Uint8Array,
|
||||
ownershipType: FileOwnershipType,
|
||||
chunkId: number,
|
||||
apiToken: string,
|
||||
): Promise<boolean> {
|
||||
const result = await this.apiService.uploadFileBytes(apiToken, ownershipType, chunkId, encryptedBytes)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ export * from './Api/FilesApiInterface'
|
||||
export * from './Api/FileSystemApi'
|
||||
export * from './Api/FileSystemNoSelection'
|
||||
export * from './Api/FileSystemResult'
|
||||
export * from './Api/DownloadFileParams'
|
||||
export * from './Api/FileOwnershipType'
|
||||
|
||||
export * from './Cache/FileMemoryCache'
|
||||
export * from './Chunker/ByteChunker'
|
||||
export * from './Chunker/OnChunkCallback'
|
||||
|
||||
Reference in New Issue
Block a user