Skip to content

Commit

Permalink
Add a Storage provider
Browse files Browse the repository at this point in the history
  • Loading branch information
curiousdannii committed Jul 14, 2024
1 parent c990763 commit 9b5ea57
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
"type": "module",
"dependencies": {
"base32768": "^3.0.1",
"lodash-es": "^4.17.21",
"mute-stream": "1.0.0"
},
Expand Down
8 changes: 6 additions & 2 deletions src/dialog/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ https://github.com/curiousdannii/asyncglk
import {AsyncDialog, DialogDirectories, DialogOptions} from '../common/interface.js'
import {DownloadOptions, DownloadProvider, ProgressCallback} from './download.js'
import {Provider} from './interface.js'
import {WebStorageProvider} from './storage.js'

export class BrowserDialog implements AsyncDialog {
'async' = true as const
Expand All @@ -26,8 +27,11 @@ export class BrowserDialog implements AsyncDialog {

async init(options: DialogOptions & DownloadOptions): Promise<void> {
this.downloader = new DownloadProvider(options)
// TODO: ensure that localStorage is wrapped in a try/catch in case it's disabled
this.providers = [
this.downloader,
new WebStorageProvider('/tmp', sessionStorage),
new WebStorageProvider('/', localStorage),
]

for (const [i, provider] of this.providers.entries()) {
Expand All @@ -47,7 +51,7 @@ export class BrowserDialog implements AsyncDialog {
}

async prompt(extension: string, save: boolean): Promise<string | null> {
throw new Error('Method not implemented.')
return prompt('Filename')
}

set_storyfile_dir(path: string): Partial<DialogDirectories> {
Expand All @@ -59,7 +63,7 @@ export class BrowserDialog implements AsyncDialog {
}

async exists(path: string): Promise<boolean> {
return (await this.providers[0].exists(path))!
return !!(await this.providers[0].exists(path))
}

async read(path: string): Promise<Uint8Array | null> {
Expand Down
11 changes: 11 additions & 0 deletions src/dialog/browser/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ export interface DirEntry {
name: string
}

export interface FileData {
accessed: number
created: number
etag?: string
data: Uint8Array
'last-modified'?: string
modified: number
path_prefix: string
story_id?: string
}

export class NullProvider implements Provider {
next: Provider = this
async delete(_path: string) {
Expand Down
109 changes: 109 additions & 0 deletions src/dialog/browser/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Web storage provider
====================
Copyright (c) 2024 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/

import {decode as base32768_decode, encode as base32768_encode} from 'base32768'

import {DirEntry, FileData, NullProvider, Provider} from './interface.js'

type WebStorageFileMetadata = Pick<FileData, 'accessed' | 'created' | 'modified'>

const METADATA_KEY = 'dialog_metadata'

const enum MetadataUpdateOperation {
DELETE = 1,
READ = 2,
WRITE = 3,
}

export class WebStorageProvider implements Provider {
next = new NullProvider()
prefix: string
store: Storage

constructor(prefix: string, store: Storage) {
this.prefix = prefix
this.store = store

// TODO: upgrade storage
}

async delete(path: string): Promise<void | null> {
if (path.startsWith(this.prefix)) {
this.store.removeItem(path)
this.update_metadata(path, MetadataUpdateOperation.DELETE)
}
else {
return this.next.delete(path)
}
}

async exists(path: string): Promise<boolean | null> {
if (path.startsWith(this.prefix)) {
return this.store.getItem(path) !== null
}
else {
return this.next.exists(path)
}
}

async list(path: string): Promise<DirEntry[] | null> {
throw new Error('Not implemented yet')
}

async read(path: string): Promise<Uint8Array | null> {
if (path.startsWith(this.prefix)) {
const res = this.store.getItem(path)
if (res !== null) {
this.update_metadata(path, MetadataUpdateOperation.READ)
return base32768_decode(res)
}
return null
}
else {
return this.next.read(path)
}
}

async write(path: string, data: Uint8Array): Promise<void | null> {
if (path.startsWith(this.prefix)) {
// TODO: detect out of space
this.store.setItem(path, base32768_encode(data))
this.update_metadata(path, MetadataUpdateOperation.WRITE)
return null
}
else {
return this.next.write(path, data)
}
}

update_metadata(path: string, op: MetadataUpdateOperation) {
const now = Date.now()
const metadata: Record<string, WebStorageFileMetadata> = JSON.parse(this.store.getItem(METADATA_KEY) || '{}')
switch (op) {
case MetadataUpdateOperation.DELETE:
delete metadata[path]
break
case MetadataUpdateOperation.READ:
metadata[path].accessed = now
break
case MetadataUpdateOperation.WRITE:
if (!metadata[path]) {
metadata[path] = {
accessed: now,
created: now,
modified: now,
}
}
metadata[path].modified = now
}
this.store.setItem(METADATA_KEY, JSON.stringify(metadata))
}
}

0 comments on commit 9b5ea57

Please sign in to comment.