sounds, better files
This commit is contained in:
100
src/model/FileSystem/FileStashFs.ts
Normal file
100
src/model/FileSystem/FileStashFs.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { extensionMimeType } from "../FileMeta/ExtensionMimeType";
|
||||
import { CachePolicy, FsCache } from "./FsCache";
|
||||
import { VirtualDirectory, VirtualFile, FileSystemSource, ListDirOptions } from "./types";
|
||||
|
||||
export type FileStashFsConfig = {
|
||||
name: string;
|
||||
domain: string;
|
||||
share: string,
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface LsResult {
|
||||
results: Array<{
|
||||
name: string,
|
||||
size: number,
|
||||
time: number,
|
||||
type: 'file' | 'directory',
|
||||
}>
|
||||
status: "ok"
|
||||
}
|
||||
|
||||
export class FileStashFs implements FileSystemSource<FileStashFsConfig> {
|
||||
|
||||
private fsCache: FsCache;
|
||||
|
||||
constructor(private options: FileStashFsConfig) {
|
||||
this.fsCache = new FsCache(options.name);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
get root(): VirtualDirectory {
|
||||
return {
|
||||
type: 'directory',
|
||||
source: this.options.name,
|
||||
path: [],
|
||||
name: this.options.name
|
||||
}
|
||||
}
|
||||
|
||||
private createUrl(type: 'cat' | 'ls', path: string[]) {
|
||||
const url = new URL(this.options.domain);
|
||||
url.pathname = `/api/files/${type}`;
|
||||
const params = new URLSearchParams();
|
||||
params.set('share', this.options.share);
|
||||
params.set('key', this.options.key);
|
||||
params.set('path', `/${path.join('/')}`);
|
||||
url.search = params.toString();
|
||||
return url;
|
||||
}
|
||||
|
||||
async listDir(dir: VirtualDirectory, {cache = CachePolicy.useNetwork}: ListDirOptions = {}): Promise<(VirtualDirectory|VirtualFile)[]> {
|
||||
const request = new Request(this.createUrl('ls', dir.path));
|
||||
const response = await this.fsCache.get(request, cache);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to list directory ${dir.path.join('/')}`);
|
||||
}
|
||||
const data = await response.json() as LsResult;
|
||||
if (data.status !== 'ok') {
|
||||
throw new Error(`Failed to list directory ${dir.path.join('/')}`);
|
||||
}
|
||||
return data.results.map((result) => {
|
||||
if (result.type === 'file') {
|
||||
return <VirtualFile>{
|
||||
type: 'file',
|
||||
source: this.options.name,
|
||||
name: result.name,
|
||||
mimeType: extensionMimeType(result.name),
|
||||
path: [...dir.path, result.name],
|
||||
}
|
||||
} else if (result.type === 'directory') {
|
||||
return {
|
||||
type: 'directory',
|
||||
source: this.options.name,
|
||||
name: result.name,
|
||||
path: [...dir.path, result.name],
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown type ${result.type}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async download(file: VirtualFile): Promise<File> {
|
||||
const response = await fetch(this.createUrl('cat', file.path));
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download file ${file.path.join('/')}`);
|
||||
}
|
||||
return new File([await response.blob()], file.name);
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return {
|
||||
...this.options,
|
||||
type: 'filestash',
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
52
src/model/FileSystem/FsCache.ts
Normal file
52
src/model/FileSystem/FsCache.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
|
||||
export const CachePolicy = {
|
||||
useCache: 'use-cache',
|
||||
useNetwork: 'use-network',
|
||||
onlyCache: 'only-cache',
|
||||
} as const;
|
||||
export type CachePolicy = typeof CachePolicy[keyof typeof CachePolicy];
|
||||
|
||||
export class CacheError extends Error {}
|
||||
export class NoCacheEntryError extends CacheError {}
|
||||
|
||||
export class FsCache {
|
||||
|
||||
private _cache?: Cache;
|
||||
|
||||
constructor(public readonly name: string) {}
|
||||
|
||||
public async cache() {
|
||||
if (!this._cache) {
|
||||
this._cache = await caches.open(this.name);
|
||||
}
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
public async fetch(request: RequestInfo): Promise<Response> {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await this.cache()
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public async get(request: RequestInfo, policy: CachePolicy): Promise<Response> {
|
||||
if (policy === CachePolicy.useNetwork) {
|
||||
return this.fetch(request);
|
||||
}
|
||||
const cache = await this.cache();
|
||||
const response = await cache.match(request);
|
||||
if (response && policy === CachePolicy.useCache) {
|
||||
return response;
|
||||
} else if (policy === CachePolicy.onlyCache) {
|
||||
if (!response) {
|
||||
throw new NoCacheEntryError('No cache entry found');
|
||||
}
|
||||
return response;
|
||||
} else {
|
||||
return this.fetch(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
129
src/model/FileSystem/MountedFs.ts
Normal file
129
src/model/FileSystem/MountedFs.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { FileSystemSource, ListDirOptions, VirtualDirectory, VirtualFile } from "./types";
|
||||
import { NginxFS } from "./NginxFS";
|
||||
import { FileStashFs } from "./FileStashFs";
|
||||
import { CachePolicy, NoCacheEntryError } from "./FsCache";
|
||||
|
||||
type Config<T> = T extends FileSystemSource<any> ? ReturnType<T['getConfig']> : never;
|
||||
|
||||
const CACHE = Symbol('Cache');
|
||||
export interface Fs {
|
||||
get root(): VirtualDirectory[];
|
||||
download(file: VirtualFile): Promise<File>;
|
||||
listDir(dir: VirtualDirectory, options?: ListDirOptions): Promise<(VirtualDirectory|VirtualFile)[]>;
|
||||
}
|
||||
|
||||
export class MountedFs implements Fs {
|
||||
private filesystems: Map<string, FileSystemSource<any>> = new Map();
|
||||
public readonly name: string = 'root';
|
||||
|
||||
constructor(filesystems: (Config<NginxFS> | Config<FileStashFs>)[]) {
|
||||
for (const fsOptions of filesystems) {
|
||||
if (fsOptions.type === 'nginx') {
|
||||
this.filesystems.set(fsOptions.name, new NginxFS(fsOptions));
|
||||
} else if (fsOptions.type === 'filestash') {
|
||||
this.filesystems.set(fsOptions.name, new FileStashFs(fsOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get root() {
|
||||
return [...this.filesystems.values()].map(fs => fs.root);
|
||||
}
|
||||
|
||||
download(file: VirtualFile) {
|
||||
const fs = this.filesystems.get(file.source);
|
||||
if (!fs) {
|
||||
throw new Error(`Filesystem ${file.source} not found`);
|
||||
}
|
||||
return fs.download(file);
|
||||
}
|
||||
|
||||
async listDir(dir: VirtualDirectory, options?: ListDirOptions) {
|
||||
|
||||
const fs = this.filesystems.get(dir.source);
|
||||
if (!fs) {
|
||||
throw new Error(`Filesystem ${dir.source} not found`);
|
||||
}
|
||||
const result = await fs.listDir(dir, options);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async *search(query: string, cancelToken?: AbortSignal) {
|
||||
query = query.toLocaleLowerCase();
|
||||
for (const fs of this.filesystems.values()) {
|
||||
const dirQueue = [fs.root];
|
||||
while (dirQueue.length > 0) {
|
||||
const dir = dirQueue.shift()!;
|
||||
try {
|
||||
const children = await this.listDir(dir, {cache: CachePolicy.onlyCache});
|
||||
for (const child of children) {
|
||||
if (cancelToken?.aborted) {
|
||||
return;
|
||||
}
|
||||
if (child.type === 'directory') {
|
||||
dirQueue.push(child);
|
||||
} else if (child.name.toLocaleLowerCase().includes(query)) {
|
||||
yield child;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!(err instanceof NoCacheEntryError)) {
|
||||
console.warn(`Failed to list directory ${dir.path.join('/')}`, err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async *buildIndex() {
|
||||
let counter = 0;
|
||||
for (const fs of this.filesystems.values()) {
|
||||
const dirQueue = [fs.root];
|
||||
while (dirQueue.length > 0) {
|
||||
const dir = dirQueue.shift()!;
|
||||
for (const child of await this.listDir(dir)) {
|
||||
if (child.type === 'directory') {
|
||||
dirQueue.push(child);
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
yield counter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// export class SearchableFilesystem implements Fs {
|
||||
// public allFiles: VirtualFile[] = [];
|
||||
|
||||
// constructor(private fs: MountedFs) {}
|
||||
|
||||
// async precacheDir(dir: VirtualDirectory) {
|
||||
// for (const child of await this.fs.listDir(dir, {cache: CachePolicy.useCache})) {
|
||||
// if (child.type === 'file') {
|
||||
// this.allFiles.push(child);
|
||||
// } else {
|
||||
// await this.precacheDir(child as VirtualDirectory);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// async search(query: string) {
|
||||
// await Promise.all(this.fs.root.map((root) => this.precacheDir(root)));
|
||||
// }
|
||||
|
||||
// get root() {
|
||||
// return this.fs.root;
|
||||
// }
|
||||
|
||||
// async listDir(dir: VirtualDirectory) {
|
||||
// return this.fs.listDir(dir);
|
||||
// }
|
||||
|
||||
// async download(file: VirtualFile) {
|
||||
// return this.fs.download(file);
|
||||
// }
|
||||
// }
|
||||
102
src/model/FileSystem/NginxFS.ts
Normal file
102
src/model/FileSystem/NginxFS.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { extensionMimeType } from "../FileMeta/ExtensionMimeType";
|
||||
import { FileSystemSource, VirtualFile, VirtualDirectory } from "./types";
|
||||
|
||||
export type NginxFsConfig = {
|
||||
name: string;
|
||||
rootUrl: string;
|
||||
auth?: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
type DateString = string & {};
|
||||
|
||||
interface NginxFile {
|
||||
name: string;
|
||||
type: 'file'
|
||||
mtime: DateString;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface NginxDirectory {
|
||||
name: string;
|
||||
type: 'directory';
|
||||
mtime: DateString;
|
||||
}
|
||||
|
||||
export class NginxFS implements FileSystemSource<NginxFsConfig> {
|
||||
constructor(private options: NginxFsConfig) {}
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
get root():VirtualDirectory {
|
||||
return {
|
||||
type: 'directory',
|
||||
source: this.options.name,
|
||||
path: [],
|
||||
name: this.options.name
|
||||
}
|
||||
}
|
||||
|
||||
private get sanitizedRootUrl() {
|
||||
return this.options.rootUrl.endsWith("/") ? this.options.rootUrl : this.options.rootUrl + "/";
|
||||
}
|
||||
|
||||
private get authHeaders(): {Authorization: string}|{} {
|
||||
return this.options.auth ? {
|
||||
Authorization: `Basic ${btoa(`${this.options.auth.username}:${this.options.auth.password}`)}`
|
||||
} : {}
|
||||
}
|
||||
|
||||
async listDir(dir: VirtualDirectory): Promise<(VirtualDirectory|VirtualFile)[]> {
|
||||
|
||||
const response = await fetch(`${this.sanitizedRootUrl}${dir.path.map((x) => `${x}/`).join("")}`, {
|
||||
headers: {
|
||||
...this.authHeaders,
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.error("Failed to list directory", response);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await response.json() as Array<NginxFile | NginxDirectory>;
|
||||
return data.map((item) => {
|
||||
if (item.type === "directory") {
|
||||
return {
|
||||
type: "directory",
|
||||
source: this.options.name,
|
||||
path: [...dir.path, item.name],
|
||||
name: item.name
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "file",
|
||||
source: this.options.name,
|
||||
path: [...dir.path, item.name],
|
||||
name: item.name,
|
||||
mimeType: extensionMimeType(item.name),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async download(file: VirtualFile): Promise<File> {
|
||||
const response = await fetch(`${this.sanitizedRootUrl}${file.path.join("/")}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to download file");
|
||||
}
|
||||
return new File([await response.blob()], file.name, { type: file.mimeType });
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return {
|
||||
...this.options,
|
||||
type: "nginx",
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/model/FileSystem/types.ts
Normal file
27
src/model/FileSystem/types.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {CachePolicy} from './FsCache';
|
||||
export interface FileSystemSource<TOptions> {
|
||||
name: string;
|
||||
root: VirtualDirectory;
|
||||
listDir(dir: VirtualDirectory, options?: ListDirOptions): Promise<(VirtualDirectory|VirtualFile)[]>;
|
||||
download(file: VirtualFile): Promise<File>;
|
||||
getConfig(): TOptions & { type: string };
|
||||
}
|
||||
|
||||
export interface ListDirOptions {
|
||||
cache?: CachePolicy
|
||||
}
|
||||
|
||||
export interface VirtualDirectory {
|
||||
type: 'directory';
|
||||
source: string;
|
||||
path: string[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface VirtualFile {
|
||||
type: 'file';
|
||||
source: string;
|
||||
path: string[];
|
||||
name: string;
|
||||
mimeType: string;
|
||||
}
|
||||
Reference in New Issue
Block a user