feat: add filepicker package

This commit is contained in:
Karol Sójko
2022-07-05 19:28:22 +02:00
parent 577da2ca84
commit d4188a3fa2
45 changed files with 5848 additions and 25 deletions

View File

@@ -0,0 +1,56 @@
import { SNApplication, ContentType, FileItem, ClientDisplayableError } from '../../../snjs'
import { ClassicFileReader, ClassicFileSaver } from '../../../filepicker'
export class ClassicFileApi {
constructor(private application: SNApplication) {
this.configureFilePicker()
}
configureFilePicker(): void {
const input = document.getElementById('filePicker') as HTMLInputElement
input.onclick = () => {
void this.openFilePicker()
}
console.log('Classic file picker ready.')
}
async openFilePicker(): Promise<void> {
const files = await ClassicFileReader.selectFiles()
for (const file of files) {
const operation = await this.application.files.beginNewFileUpload()
if (operation instanceof ClientDisplayableError) {
continue
}
const fileResult = await ClassicFileReader.readFile(file, 2_000_000, async (chunk, index, isLast) => {
await this.application.files.pushBytesForUpload(operation, chunk, index, isLast)
})
const snFile = await this.application.files.finishUpload(operation, fileResult)
if (snFile instanceof ClientDisplayableError) {
return
}
const bytes = await this.downloadFileBytes(snFile.remoteIdentifier)
new ClassicFileSaver().saveFile(`${snFile.name}.${snFile.ext}`, bytes)
}
}
downloadFileBytes = async (remoteIdentifier: string): Promise<Uint8Array> => {
console.log('Downloading file', remoteIdentifier)
const file = this.application['itemManager']
.getItems(ContentType.File)
.find((file: FileItem) => file.remoteIdentifier === remoteIdentifier)
let receivedBytes = new Uint8Array()
await this.application.files.downloadFile(file, async (decryptedBytes: Uint8Array) => {
console.log(`Downloaded ${decryptedBytes.length} bytes`)
receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes])
})
console.log('Successfully downloaded and decrypted file!')
return receivedBytes
}
}

View File

@@ -0,0 +1,68 @@
import { StreamingFileReader, StreamingFileSaver } from '../../../filepicker'
import { SNApplication, FileItem, ClientDisplayableError } from '../../../snjs'
export class FileSystemApi {
private uploadedFiles: FileItem[] = []
constructor(private application: SNApplication) {
this.configureFilePicker()
this.configureDownloadButton()
}
get downloadButton(): HTMLButtonElement {
return document.getElementById('downloadButton') as HTMLButtonElement
}
configureDownloadButton(): void {
this.downloadButton.onclick = this.downloadFiles
this.downloadButton.style.display = 'none'
}
configureFilePicker(): void {
const button = document.getElementById('fileSystemUploadButton') as HTMLButtonElement
button.onclick = this.uploadFiles
console.log('File picker ready.')
}
uploadFiles = async (): Promise<void> => {
const snFiles = []
const selectedFiles = await StreamingFileReader.selectFiles()
for (const file of selectedFiles) {
const operation = await this.application.files.beginNewFileUpload()
if (operation instanceof ClientDisplayableError) {
continue
}
const fileResult = await StreamingFileReader.readFile(file, 2_000_000, async (chunk, index, isLast) => {
await this.application.files.pushBytesForUpload(operation, chunk, index, isLast)
})
const snFile = await this.application.files.finishUpload(operation, fileResult)
snFiles.push(snFile)
}
this.downloadButton.style.display = ''
this.uploadedFiles = snFiles
}
downloadFiles = async (): Promise<void> => {
for (const snFile of this.uploadedFiles) {
console.log('Downloading file', snFile.remoteIdentifier)
const saver = new StreamingFileSaver(snFile.name)
await saver.selectFileToSaveTo()
saver.loggingEnabled = true
await this.application.files.downloadFile(snFile, async (decryptedBytes: Uint8Array) => {
console.log(`Pushing ${decryptedBytes.length} decrypted bytes to disk`)
await saver.pushBytes(decryptedBytes)
})
console.log('Closing file saver reader')
await saver.finish()
console.log('Successfully downloaded and decrypted file!')
}
}
}

View File

@@ -0,0 +1,89 @@
import { SNApplication, Environment, Platform, SNLog } from '../../../snjs'
import WebDeviceInterface from './web_device_interface'
import { SNWebCrypto } from '../../../sncrypto-web'
import { ClassicFileApi } from './classic_file_api'
import { FileSystemApi } from './file_system_api'
SNLog.onLog = console.log
SNLog.onError = console.error
console.log('Clearing localStorage...')
localStorage.clear()
/**
* Important:
* If reusing e2e docker servers, you must edit docker/auth.env ACCESS_TOKEN_AGE
* and REFRESH_TOKEN_AGE and increase their ttl.
*/
const host = 'http://localhost:3123'
const mocksHost = 'http://localhost:3124'
const application = new SNApplication({
environment: Environment.Web,
platform: Platform.MacWeb,
deviceInterface: new WebDeviceInterface(),
crypto: new SNWebCrypto(),
alertService: {
confirm: async () => true,
alert: async () => {
alert()
},
blockingDialog: () => () => {
confirm()
},
},
identifier: `${Math.random()}`,
defaultHost: host,
appVersion: '1.0.0',
})
console.log('Created application', application)
export async function publishMockedEvent(eventType: string, eventPayload: unknown): Promise<void> {
await fetch(`${mocksHost}/events`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
eventType,
eventPayload,
}),
})
}
const run = async () => {
console.log('Preparing for launch...')
await application.prepareForLaunch({
receiveChallenge: () => {
console.warn('Ignoring challenge')
},
})
await application.launch()
console.log('Application launched...')
const email = String(Math.random())
const password = String(Math.random())
console.log('Registering account...')
await application.register(email, password)
console.log(`Registered account ${email}/${password}. Be sure to edit docker/auth.env to increase session TTL.`)
console.log('Creating mock subscription...')
await publishMockedEvent('SUBSCRIPTION_PURCHASED', {
userEmail: email,
subscriptionId: 1,
subscriptionName: 'PLUS_PLAN',
subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000,
timestamp: Date.now(),
offline: false,
})
console.log('Successfully created mock subscription...')
new ClassicFileApi(application)
new FileSystemApi(application)
}
void run()

View File

@@ -0,0 +1,138 @@
/* eslint-disable no-undef */
const KEYCHAIN_STORAGE_KEY = 'keychain'
export default class WebDeviceInterface {
async getRawStorageValue(key) {
return localStorage.getItem(key)
}
async getJsonParsedRawStorageValue(key) {
const value = await this.getRawStorageValue(key)
if (isNullOrUndefined(value)) {
return undefined
}
try {
return JSON.parse(value)
} catch (e) {
return value
}
}
async getAllRawStorageKeyValues() {
const results = []
for (const key of Object.keys(localStorage)) {
results.push({
key: key,
value: localStorage[key],
})
}
return results
}
async setRawStorageValue(key, value) {
localStorage.setItem(key, value)
}
async removeRawStorageValue(key) {
localStorage.removeItem(key)
}
async removeAllRawStorageValues() {
localStorage.clear()
}
async openDatabase(_identifier) {
return {}
}
_getDatabaseKeyPrefix(identifier) {
if (identifier) {
return `${identifier}-item-`
} else {
return 'item-'
}
}
_keyForPayloadId(id, identifier) {
return `${this._getDatabaseKeyPrefix(identifier)}${id}`
}
async getAllRawDatabasePayloads(identifier) {
const models = []
for (const key in localStorage) {
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
models.push(JSON.parse(localStorage[key]))
}
}
return models
}
async saveRawDatabasePayload(payload, identifier) {
localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload))
}
async saveRawDatabasePayloads(payloads, identifier) {
for (const payload of payloads) {
await this.saveRawDatabasePayload(payload, identifier)
}
}
async removeRawDatabasePayloadWithId(id, identifier) {
localStorage.removeItem(this._keyForPayloadId(id, identifier))
}
async removeAllRawDatabasePayloads(identifier) {
for (const key in localStorage) {
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
delete localStorage[key]
}
}
}
/** @keychain */
async getNamespacedKeychainValue(identifier) {
const keychain = await this.getRawKeychainValue(identifier)
if (!keychain) {
return
}
return keychain[identifier]
}
async setNamespacedKeychainValue(value, identifier) {
let keychain = await this.getRawKeychainValue()
if (!keychain) {
keychain = {}
}
localStorage.setItem(
KEYCHAIN_STORAGE_KEY,
JSON.stringify({
...keychain,
[identifier]: value,
}),
)
}
async clearNamespacedKeychainValue(identifier) {
const keychain = await this.getRawKeychainValue()
if (!keychain) {
return
}
delete keychain[identifier]
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain))
}
/** Allows unit tests to set legacy keychain structure as it was <= 003 */
// eslint-disable-next-line camelcase
async setLegacyRawKeychainValue(value) {
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
}
async getRawKeychainValue() {
const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
return JSON.parse(keychain)
}
async clearRawKeychainValue() {
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
}
}