feat: add filepicker package
This commit is contained in:
56
packages/filepicker/example/src/classic_file_api.ts
Normal file
56
packages/filepicker/example/src/classic_file_api.ts
Normal 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
|
||||
}
|
||||
}
|
||||
68
packages/filepicker/example/src/file_system_api.ts
Normal file
68
packages/filepicker/example/src/file_system_api.ts
Normal 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!')
|
||||
}
|
||||
}
|
||||
}
|
||||
89
packages/filepicker/example/src/index.ts
Normal file
89
packages/filepicker/example/src/index.ts
Normal 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()
|
||||
138
packages/filepicker/example/src/web_device_interface.js
Normal file
138
packages/filepicker/example/src/web_device_interface.js
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user