Skip to content

Commit

Permalink
Asset support
Browse files Browse the repository at this point in the history
  • Loading branch information
ObsidianSnoo committed May 14, 2024
1 parent 880c3ff commit fb6e184
Show file tree
Hide file tree
Showing 23 changed files with 5,370 additions and 2,030 deletions.
5,802 changes: 4,207 additions & 1,595 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
"@codemirror/lint": "6.5.0",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.3",
"@devvit/previews": "0.10.20",
"@devvit/protos": "0.10.20",
"@devvit/public-api": "0.10.20",
"@devvit/runtime-lite": "0.10.20",
"@devvit/shared-types": "0.10.20",
"@devvit/ui-renderer": "0.10.20",
"@devvit/previews": "0.10.21-next-2024-05-13-b48ce196f.0",
"@devvit/protos": "0.10.21-next-2024-05-13-b48ce196f.0",
"@devvit/public-api": "0.10.21-next-2024-05-13-b48ce196f.0",
"@devvit/runtime-lite": "0.10.21-next-2024-05-13-b48ce196f.0",
"@devvit/shared-types": "0.10.21-next-2024-05-13-b48ce196f.0",
"@devvit/ui-renderer": "0.10.21-next-2024-05-13-b48ce196f.0",
"@esm-bundle/chai": "4.3.4-fix.0",
"@types/jsdom": "21.1.6",
"@types/mocha": "10.0.6",
Expand Down
119 changes: 109 additions & 10 deletions src/assets/asset-manager.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import type {AssetMap} from '@devvit/shared-types/Assets.js'
import * as ZenFS from '@zenfs/core'
import {FileSystem, fs} from '@zenfs/core'
import {WebStorage, WebAccess} from '@zenfs/dom'
import {WebAccess, WebStorage} from '@zenfs/dom'
import {Zip} from '@zenfs/zip'
import * as IDB from 'idb-keyval'
import {hasFileAccessAPI} from '../elements/play-assets/file-access-api.js'
import {hasFileAccessAPI, tryQueryPermission} from '../utils/file-access-api.js'

export type AssetFilesystemType = 'virtual' | 'local'
export type LocalSourceType = 'directory' | 'archive' | undefined

const LOG_TAG = '[AssetManager]'

const CACHE_LAST_DIR = 'lastMountedDirectory'
const CACHE_LAST_ZIP = 'lastMountedArchive'

export class AssetManager {
private static _waitForInit: Promise<void> = new Promise(resolve => {
this._initComplete = resolve
})
private static _initComplete: () => void
private static _filesystemType: AssetFilesystemType = 'virtual'
private static _assetMap: AssetMap = {}
private static _assetCount: number = 0
Expand Down Expand Up @@ -57,7 +60,7 @@ export class AssetManager {
}

static get assetMap(): Promise<AssetMap> {
const resolvedMap = () => Promise.resolve(this._assetMap)
const resolvedMap = () => this._waitForInit.then(() => this._assetMap)
if (this.isDirectoryMounted) {
return this._updateMap().then(resolvedMap)
}
Expand All @@ -75,11 +78,30 @@ export class AssetManager {
])

if (lastMountedDirectory) {
await this.mountLocalDirectory(lastMountedDirectory)
if (await this._verifyPermissions(lastMountedDirectory)) {
await this.mountLocalDirectory(lastMountedDirectory)
} else {
await IDB.del(CACHE_LAST_DIR)
console.debug(
LOG_TAG,
'Failed to remount the previously used directory'
)
}
} else if (lastMountedArchive) {
await this.mountLocalArchive(lastMountedArchive)
if (await this._verifyPermissions(lastMountedArchive)) {
await this.mountLocalArchive(lastMountedArchive)
} else {
await IDB.del(CACHE_LAST_ZIP)
console.debug(
LOG_TAG,
'Failed to remount the previously used ZIP file'
)
}
} else {
this._emitChangeEvent()
}
}
this._initComplete()
}

static addEventListener(
Expand Down Expand Up @@ -146,6 +168,67 @@ export class AssetManager {
this._emitChangeEvent()
}

static async addVirtualAsset(
handle: File | FileSystemFileHandle
): Promise<void> {
let file: File
if ('getFile' in handle) {
file = await handle.getFile()
} else {
file = handle
}

await fs.promises.writeFile(
file.name,
new Uint8Array(await file.arrayBuffer()),
{flush: true}
)

await this._updateMap()
this._emitChangeEvent()
}

static async renameVirtualAsset(
oldName: string,
newName: string
): Promise<void> {
if (this.filesystemType !== 'virtual') {
return
}

await fs.promises.rename(oldName, newName)

await this._updateMap()
this._emitChangeEvent()
}

static async deleteVirtualAsset(name: string): Promise<void> {
if (this.filesystemType !== 'virtual') {
return
}

await fs.promises.unlink(name)

await this._updateMap()
this._emitChangeEvent()
}

static async clearVirtualAssets(): Promise<void> {
if (this.filesystemType !== 'virtual' || !this._assetMap) {
return
}

const removals = []
for (const name of Object.keys(this._assetMap)) {
removals.push(fs.promises.unlink(name))
}

await Promise.all(removals)

await this._updateMap()
this._emitChangeEvent()
}

private static async _mountRoot(fs: FileSystem): Promise<void> {
this._unmountRoot()
ZenFS.mount('/', fs)
Expand All @@ -161,8 +244,7 @@ export class AssetManager {
}

private static async _updateMap(): Promise<void> {
this._assetMap = {}
this._assetCount = 0
this._clearAssetMap()
if (!ZenFS.mounts.has('/')) {
console.debug(LOG_TAG, 'Root filesystem unmounted, skipping _updateMap')
return
Expand All @@ -185,14 +267,25 @@ export class AssetManager {
const file = await fs.promises.open(entry)
const buffer = await file.readFile()
await file.close()
const url = window.URL.createObjectURL(new Blob([buffer]))
this._assetMap[name] = url
this._assetMap![name] = window.URL.createObjectURL(new Blob([buffer]))
this._assetCount++
}
}
console.debug(LOG_TAG, `Found ${this._assetCount} files`, this._assetMap)
}

private static _clearAssetMap() {
const assetMap = this._assetMap!
for (const entry of Object.keys(assetMap)) {
const url = assetMap[entry] ?? undefined
if (url) {
URL.revokeObjectURL(url)
}
}
this._assetMap = {}
this._assetCount = 0
}

private static async _updateCache(handles: {
directory?: FileSystemDirectoryHandle | undefined
file?: FileSystemFileHandle | File | undefined
Expand All @@ -206,4 +299,10 @@ export class AssetManager {
private static _emitChangeEvent(): void {
this._eventTarget.dispatchEvent(new CustomEvent('change'))
}

private static async _verifyPermissions(
handle: FileSystemHandle
): Promise<boolean> {
return (await tryQueryPermission(handle, {mode: 'read'})) === 'granted'
}
}
7 changes: 7 additions & 0 deletions src/elements/play-assets-dialog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {assert} from '@esm-bundle/chai'
import {PlayAssetsDialog} from './play-assets-dialog'

test('tag is defined', () => {
const el = document.createElement('play-assets-dialog')
assert.instanceOf(el, PlayAssetsDialog)
})
36 changes: 28 additions & 8 deletions src/elements/play-assets-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import {customElement, property, state} from 'lit/decorators.js'
import {css, type CSSResultGroup, html, type TemplateResult} from 'lit'
import {PlayDialog} from './play-dialog/play-dialog.js'
import {choose} from 'lit-html/directives/choose.js'

import './play-assets/play-assets-local-fs.js'
import './play-assets/play-assets-virtual-fs.js'
import {when} from 'lit-html/directives/when.js'
import {
type AssetFilesystemType,
AssetManager
} from '../assets/asset-manager.js'

import './play-assets/play-assets-virtual-fs.js'
import './play-assets/play-assets-local-directory.js'
import './play-assets/play-assets-local-archive.js'

declare global {
interface HTMLElementEventMap {}
interface HTMLElementTagNameMap {
Expand All @@ -26,6 +27,12 @@ export class PlayAssetsDialog extends PlayDialog {
fieldset {
margin-bottom: var(--space);
}
#localFs {
display: flex;
flex-direction: column;
gap: 8px;
}
`

@property({attribute: 'enable-local-assets', type: Boolean})
Expand All @@ -37,14 +44,14 @@ export class PlayAssetsDialog extends PlayDialog {
override connectedCallback() {
super.connectedCallback()

AssetManager.addEventListener('change', this._updateAssetManagerState)
this._updateAssetManagerState()
AssetManager.addEventListener('change', this._updateMountState)
this._updateMountState()
}

override disconnectedCallback() {
super.disconnectedCallback()

AssetManager.removeEventListener('change', this._updateAssetManagerState)
AssetManager.removeEventListener('change', this._updateMountState)
}

override get dialogTitle(): string {
Expand All @@ -66,7 +73,7 @@ export class PlayAssetsDialog extends PlayDialog {
'virtual',
() => html`<play-assets-virtual-fs></play-assets-virtual-fs>`
],
['local', () => html`<play-assets-local-fs></play-assets-local-fs>`]
['local', this._renderLocalFs]
])}
</fieldset>
`
Expand Down Expand Up @@ -104,11 +111,24 @@ export class PlayAssetsDialog extends PlayDialog {
AssetManager.filesystemType = ev.currentTarget.value as AssetFilesystemType
}

private _renderLocalFs = () => {
return html`
<div id="localFs">
${when(
AssetManager.hasFileAccessAPI,
() =>
html`<play-assets-local-directory></play-assets-local-directory>`
)}
<play-assets-local-archive></play-assets-local-archive>
</div>
`
}

private get _filesystemTitle(): string {
return this._filesystem === 'virtual' ? 'Manage files' : 'Filesystem source'
}

private _updateAssetManagerState = () => {
private _updateMountState = () => {
this._filesystem = AssetManager.filesystemType
}
}
74 changes: 0 additions & 74 deletions src/elements/play-assets/file-access-api.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/elements/play-assets/file-upload-dropper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {assert} from '@esm-bundle/chai'
import {FileUploadDropper} from './file-upload-dropper'

test('tag is defined', () => {
const el = document.createElement('file-upload-dropper')
assert.instanceOf(el, FileUploadDropper)
})
Loading

0 comments on commit fb6e184

Please sign in to comment.