packages/web-ide-fs/src/browserfs/GitLabReadableFileSystem.ts (169 lines of code) (raw):

import { splitParent } from '@gitlab/utils-path'; import { ApiError, ErrorCode } from 'browserfs/dist/node/core/api_error'; import type { FileFlag } from 'browserfs/dist/node/core/file_flag'; import { ActionType } from 'browserfs/dist/node/core/file_flag'; import type { BFSCallback } from 'browserfs/dist/node/core/file_system'; import { BaseFileSystem } from 'browserfs/dist/node/core/file_system'; import type { File } from 'browserfs/dist/node/core/file'; import Stats, { FileType as BrowserFSFileType } from 'browserfs/dist/node/core/node_fs_stats'; import { NoSyncFile } from 'browserfs/dist/node/generic/preload_file'; import { copyingSlice } from 'browserfs/dist/node/core/util'; import type { FileContentProvider } from '../types'; import { FileType } from '../types'; import type { FileEntry, MutableFileEntry } from '../utils'; import { BlobContentType } from '../utils'; export const DEFAULT_DATE = new Date(0); const convertFileEntryToStats = (entry: FileEntry): Stats => { // TODO: let's figure out what mode should be if file type is a tree. Currently this isn't handled in `createFileEntryMap` const mode = entry.type === FileType.Blob ? entry.mode : undefined; const atime = DEFAULT_DATE; const mtime = atime; const ctime = atime; const stats = new Stats( entry.type === FileType.Blob ? BrowserFSFileType.FILE : BrowserFSFileType.DIRECTORY, // TODO: let's figure out what to do with size. It turns out that -1 is a flag for "use whatever fileData says" entry.type === FileType.Blob ? -1 : 4096, mode, atime, mtime, ctime, ); if (entry.type === FileType.Tree) { return stats; } if (entry.content.type === BlobContentType.Raw) { stats.fileData = <Buffer>entry.content.raw; stats.size = stats.fileData.length; } return stats; }; export interface GitLabReadableFileSystemOptions { entries: Map<string, MutableFileEntry>; contentProvider: FileContentProvider; } /** * This is a BrowserFS File System for reading from "deferred" GitLab file entries. * * See https://github.com/jvilk/BrowserFS/blob/a96aa2d417995dac7d376987839bc4e95e218e06/src/backend/HTTPRequest.ts * for where this implementation is inspired from. */ export class GitLabReadableFileSystem extends BaseFileSystem { public static readonly Name = 'GitLabReadableFileSystem'; // BrowserFS likes static Create functions for these public static Create( opts: GitLabReadableFileSystemOptions, cb: BFSCallback<GitLabReadableFileSystem>, ): void { cb(null, new GitLabReadableFileSystem(opts.entries, opts.contentProvider)); } readonly #entries: Map<string, MutableFileEntry>; readonly #contentProvider: FileContentProvider; // eslint-disable-next-line no-restricted-syntax private constructor( entries: Map<string, MutableFileEntry>, contentProvider: FileContentProvider, ) { super(); this.#entries = entries; this.#contentProvider = contentProvider; } // eslint-disable-next-line class-methods-use-this public isReadOnly(): boolean { return true; } // eslint-disable-next-line class-methods-use-this public supportsLinks(): boolean { return false; } // eslint-disable-next-line class-methods-use-this public supportsProps(): boolean { return false; } // eslint-disable-next-line class-methods-use-this public supportsSynch(): boolean { return false; } // eslint-disable-next-line class-methods-use-this public getName(): string { return GitLabReadableFileSystem.Name; } // eslint-disable-next-line class-methods-use-this public diskSpace(path: string, cb: (total: number, free: number) => void): void { // Read-only file system. We could calculate the total space, but that's not // important right now. cb(0, 0); } // eslint-disable-next-line consistent-return public stat(path: string, isLstat: boolean, cb: BFSCallback<Stats>): void { const fileEntry = this.#entries.get(path); if (!fileEntry) { return cb(ApiError.ENOENT(path)); } cb(null, convertFileEntryToStats(fileEntry)); } // eslint-disable-next-line consistent-return public open(path: string, flags: FileFlag, mode: number, cb: BFSCallback<File>): void { // INVARIANT: You can't write to files on this file system. if (flags.isWriteable()) { return cb(new ApiError(ErrorCode.EPERM, path)); } // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // Check if the path exists, and is a file. const fileEntry = this.#entries.get(path); if (!fileEntry) { return cb(ApiError.ENOENT(path)); } if (fileEntry.type === FileType.Blob) { switch (flags.pathExistsAction()) { case ActionType.THROW_EXCEPTION: case ActionType.TRUNCATE_FILE: return cb(ApiError.EEXIST(path)); case ActionType.NOP: // Use existing file contents. // XXX: Uh, this maintains the previously-used flag. if (fileEntry.content.type === BlobContentType.Raw) { const stats = convertFileEntryToStats(fileEntry); return cb(null, new NoSyncFile(self, path, flags, stats, stats.fileData || undefined)); } this.#contentProvider .getContent(fileEntry.content.path) .then(raw => { // TODO do something with file size // fileEntry.size = ...; fileEntry.content = { type: BlobContentType.Raw, raw, }; const stats = convertFileEntryToStats(fileEntry); return cb( null, new NoSyncFile(self, path, flags, stats, stats.fileData || undefined), ); }) .catch(err => cb(err)); break; default: return cb(new ApiError(ErrorCode.EINVAL, 'Invalid FileMode object.')); } } else { return cb(ApiError.EISDIR(path)); } } public readdir(path: string, cb: BFSCallback<string[]>): void { // Check if it exists. const fileEntry = this.#entries.get(path); if (!fileEntry) { cb(ApiError.ENOENT(path)); } else if (fileEntry.type === FileType.Tree) { const childNames = fileEntry.children.map(x => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [parent, name] = splitParent(x); return name; }); cb(null, childNames); } else { cb(ApiError.ENOTDIR(path)); } } public readFile( fname: string, encoding: string, flag: FileFlag, cb: BFSCallback<string | Buffer>, ): void { // Get file. // eslint-disable-next-line consistent-return this.open(fname, flag, 0x1a4, (err: ApiError | undefined | null, fd?: File) => { if (err) { return cb(err); } const fdCast = <NoSyncFile<GitLabReadableFileSystem>>fd; const fdBuff = <Buffer>fdCast.getBuffer(); if (encoding === null) { cb(null, copyingSlice(fdBuff)); } else { try { const str = fdBuff.toString(<BufferEncoding>encoding); cb(null, str); } catch { cb( new ApiError( ErrorCode.EINVAL, `Could not convert buffer to string (path: ${fname}, encoding: ${encoding})`, ), ); } } }); } }