Skip to content
This repository was archived by the owner on Jul 6, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"printWidth": 120,
"semi": false
}
6 changes: 5 additions & 1 deletion packages/filepicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"watch": "tsc -p tsconfig.json --watch",
"build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts",
"test": "jest"
"test:unit": "jest"
},
"devDependencies": {
"@standardnotes/config": "^2.4.0",
Expand All @@ -38,5 +38,9 @@
"/node_modules/",
"/example/"
]
},
"dependencies": {
"@standardnotes/common": "^1.19.5",
"@standardnotes/utils": "^1.6.1"
}
}
77 changes: 77 additions & 0 deletions packages/filepicker/src/Cache/FileMemoryCache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { FileMemoryCache } from './FileMemoryCache'

describe('file memory cache', () => {
const createFile = (size: number): Uint8Array => {
return new TextEncoder().encode('a'.repeat(size))
}

it('should add file', () => {
const cache = new FileMemoryCache(5)
const file = createFile(1)
cache.add('123', file)

expect(cache.get('123')).toEqual(file)
})

it('should fail to add file if exceeds maximum', () => {
const maxSize = 5
const cache = new FileMemoryCache(maxSize)
const file = createFile(maxSize + 1)

expect(cache.add('123', file)).toEqual(false)
})

it('should allow filling files up to limit', () => {
const cache = new FileMemoryCache(5)

cache.add('1', createFile(3))
cache.add('2', createFile(2))

expect(cache.get('1')).toBeTruthy()
expect(cache.get('2')).toBeTruthy()
})

it('should clear early files when adding new files above limit', () => {
const cache = new FileMemoryCache(5)

cache.add('1', createFile(3))
cache.add('2', createFile(2))
cache.add('3', createFile(5))

expect(cache.get('1')).toBeFalsy()
expect(cache.get('2')).toBeFalsy()
expect(cache.get('3')).toBeTruthy()
})

it('should remove single file', () => {
const cache = new FileMemoryCache(5)

cache.add('1', createFile(3))
cache.add('2', createFile(2))

cache.remove('1')

expect(cache.get('1')).toBeFalsy()
expect(cache.get('2')).toBeTruthy()
})

it('should clear all files', () => {
const cache = new FileMemoryCache(5)

cache.add('1', createFile(3))
cache.add('2', createFile(2))
cache.clear()

expect(cache.get('1')).toBeFalsy()
expect(cache.get('2')).toBeFalsy()
})

it('should return correct size', () => {
const cache = new FileMemoryCache(20)

cache.add('1', createFile(3))
cache.add('2', createFile(10))

expect(cache.size).toEqual(13)
})
})
47 changes: 47 additions & 0 deletions packages/filepicker/src/Cache/FileMemoryCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { removeFromArray } from '@standardnotes/utils'
import { Uuid } from '@standardnotes/common'

export class FileMemoryCache {
private cache: Record<Uuid, Uint8Array> = {}
private orderedQueue: Uuid[] = []

constructor(public readonly maxSize: number) {}

add(uuid: Uuid, data: Uint8Array): boolean {
if (data.length > this.maxSize) {
return false
}

while (this.size + data.length > this.maxSize) {
this.remove(this.orderedQueue[0])
}

this.cache[uuid] = data

this.orderedQueue.push(uuid)

return true
}

get size(): number {
return Object.values(this.cache)
.map((bytes) => bytes.length)
.reduce((total, fileLength) => total + fileLength, 0)
}

get(uuid: Uuid): Uint8Array | undefined {
return this.cache[uuid]
}

remove(uuid: Uuid): void {
delete this.cache[uuid]

removeFromArray(this.orderedQueue, uuid)
}

clear(): void {
this.cache = {}

this.orderedQueue = []
}
}
1 change: 1 addition & 0 deletions packages/filepicker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Streaming/StreamingReader'
export * from './Streaming/StreamingSaver'
export * from './utils'
export * from './Chunker/ByteChunker'
export * from './Cache/FileMemoryCache'
2 changes: 1 addition & 1 deletion packages/snjs/.prettierrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"printWidth": 120,
"semi": false
}
59 changes: 53 additions & 6 deletions packages/snjs/lib/Services/Files/FileService.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { InternalEventBusInterface } from '@standardnotes/services'
import { SNFileService } from './FileService'
import { SNSyncService } from '../Sync/SyncService'
import { ItemManager, SNAlertService, SNApiService } from '@Lib/index'
import { SNPureCrypto, StreamEncryptor } from '@standardnotes/sncrypto-common'
import { SNFile } from '@standardnotes/models'

describe('fileService', () => {
let apiService: SNApiService
let itemManager: ItemManager
let syncService: SNSyncService
let alertService: SNAlertService
let crypto: SNPureCrypto
let fileService: SNFileService
let internalEventBus: InternalEventBusInterface

beforeEach(() => {
apiService = {} as jest.Mocked<SNApiService>
apiService.addEventObserver = jest.fn()
apiService.createFileValetToken = jest.fn()
apiService.downloadFile = jest.fn()
apiService.deleteFile = jest.fn().mockReturnValue({})

itemManager = {} as jest.Mocked<ItemManager>
itemManager.createItem = jest.fn()
itemManager.createTemplateItem = jest.fn().mockReturnValue({})
itemManager.setItemsToBeDeleted = jest.fn()
itemManager.setItemToBeDeleted = jest.fn()
itemManager.addObserver = jest.fn()
itemManager.changeItem = jest.fn()

Expand All @@ -29,14 +37,16 @@ describe('fileService', () => {

crypto = {} as jest.Mocked<SNPureCrypto>
crypto.base64Decode = jest.fn()
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publish = jest.fn()

fileService = new SNFileService(apiService, itemManager, syncService, alertService, crypto, internalEventBus)

crypto.xchacha20StreamInitDecryptor = jest.fn().mockReturnValue({
state: {},
} as StreamEncryptor)

crypto.xchacha20StreamDecryptorPush = jest
.fn()
.mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 })
crypto.xchacha20StreamDecryptorPush = jest.fn().mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 })

crypto.xchacha20StreamInitEncryptor = jest.fn().mockReturnValue({
header: 'some-header',
Expand All @@ -46,6 +56,43 @@ describe('fileService', () => {
crypto.xchacha20StreamEncryptorPush = jest.fn().mockReturnValue(new Uint8Array())
})

// eslint-disable-next-line @typescript-eslint/no-empty-function
it('placeholder', async () => {})
it('should cache file after download', async () => {
const file = {
uuid: '1',
size: 100_000,
} as jest.Mocked<SNFile>

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['cache'].get(file.uuid)).toBeTruthy()
})

it('deleting file should remove it from cache', async () => {
const file = {
uuid: '1',
size: 100_000,
} as jest.Mocked<SNFile>

await fileService.downloadFile(file, async () => {
return Promise.resolve()
})

await fileService.deleteFile(file)

expect(fileService['cache'].get(file.uuid)).toBeFalsy()
})
})
63 changes: 39 additions & 24 deletions packages/snjs/lib/Services/Files/FileService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FileMemoryCache } from '@standardnotes/filepicker'
import { FilesServerInterface } from './FilesServerInterface'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
Expand All @@ -19,7 +20,11 @@ import { UuidGenerator } from '@standardnotes/utils'
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
import { FilesClientInterface } from './FilesClientInterface'

const OneHundredMb = 100 * 1_000_000

export class SNFileService extends AbstractService implements FilesClientInterface {
private cache: FileMemoryCache = new FileMemoryCache(OneHundredMb)

constructor(
private api: FilesServerInterface,
private itemManager: ItemManager,
Expand All @@ -33,6 +38,9 @@ export class SNFileService extends AbstractService implements FilesClientInterfa

override deinit(): void {
super.deinit()

this.cache.clear()
;(this.cache as unknown) = undefined
;(this.api as unknown) = undefined
;(this.itemManager as unknown) = undefined
;(this.syncService as unknown) = undefined
Expand All @@ -44,9 +52,7 @@ export class SNFileService extends AbstractService implements FilesClientInterfa
return 5_000_000
}

public async beginNewFileUpload(): Promise<
EncryptAndUploadFileOperation | ClientDisplayableError
> {
public async beginNewFileUpload(): Promise<EncryptAndUploadFileOperation | ClientDisplayableError> {
const remoteIdentifier = UuidGenerator.GenerateUuid()
const tokenResult = await this.api.createFileValetToken(remoteIdentifier, 'write')

Expand All @@ -60,12 +66,7 @@ export class SNFileService extends AbstractService implements FilesClientInterfa
remoteIdentifier,
}

const uploadOperation = new EncryptAndUploadFileOperation(
fileParams,
tokenResult,
this.crypto,
this.api,
)
const uploadOperation = new EncryptAndUploadFileOperation(fileParams, tokenResult, this.crypto, this.api)

uploadOperation.initializeHeader()

Expand Down Expand Up @@ -126,30 +127,44 @@ export class SNFileService extends AbstractService implements FilesClientInterfa
file: SNFile,
onDecryptedBytes: (bytes: Uint8Array) => Promise<void>,
): Promise<ClientDisplayableError | undefined> {
const cachedFile = this.cache.get(file.uuid)

if (cachedFile) {
await onDecryptedBytes(cachedFile)

return undefined
}

const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'read')

if (tokenResult instanceof ClientDisplayableError) {
return tokenResult
}

// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const operation = new DownloadAndDecryptFileOperation(
file,
this.crypto,
this.api,
tokenResult,
onDecryptedBytes,
(error) => {
resolve(error)
},
)

await operation.run().then(() => resolve(undefined))
})
const addToCache = file.size < this.cache.maxSize
let cacheEntryAggregate = new Uint8Array()

const bytesWrapper = async (bytes: Uint8Array): Promise<void> => {
if (addToCache) {
cacheEntryAggregate = new Uint8Array([...cacheEntryAggregate, ...bytes])
}
return onDecryptedBytes(bytes)
}

const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api, tokenResult)

const result = await operation.run(bytesWrapper)

if (addToCache) {
this.cache.add(file.uuid, cacheEntryAggregate)
}

return result.error
}

public async deleteFile(file: SNFile): Promise<ClientDisplayableError | undefined> {
this.cache.remove(file.uuid)

const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'delete')

if (tokenResult instanceof ClientDisplayableError) {
Expand Down
Loading