import { existsSync } from "fs" ; import path from "path" ; import { MiniflareError , StoredKeyMeta , StoredMeta , StoredValueMeta , defaultClock , sanitisePath , viewToArray , } from "@miniflare/shared" ; import { LocalStorage } from "@miniflare/storage-memory" ; import { deleteFile , readFile , walk , writeFile } from "./helpers" ; const metaSuffix = ".meta.json" ; export type FileStorageErrorCode = "ERR_TRAVERSAL" | "ERR_NAMESPACE_KEY_CHILD" ; export class FileStorageError extends MiniflareError < FileStorageErrorCode > { } interface FileMeta < Meta = unknown > extends StoredMeta < Meta > { key ? : string ; } export class FileStorage extends LocalStorage { protected readonly root : string ; constructor ( root , private readonly sanitise = true , clock = defaultClock ) { super ( clock ) ; this . root = path . resolve ( root ) ; } private keyPath ( key ) { const sanitisedKey = this . sanitise ? sanitisePath ( key ) : key ; const filePath = path . join ( this . root , sanitisedKey ) ; return [ filePath . startsWith ( this . root ) ? filePath : undefined , sanitisedKey !== key , ] ; } private async meta < Meta > ( keyFilePath ) { const metaString = await readFile ( keyFilePath + metaSuffix , true ) ; return metaString ? JSON . parse ( metaString ) : { } ; } async hasMaybeExpired ( key ) { const [ filePath ] = this . keyPath ( key ) ; if ( ! filePath ) return ; if ( ! existsSync ( filePath ) ) return ; const meta = await this . meta ( filePath ) ; return { expiration : meta . expiration , metadata : meta . metadata } ; } async getMaybeExpired < Meta > ( key ) { const [ filePath ] = this . keyPath ( key ) ; if ( ! filePath ) return ; try { const value = await readFile ( filePath ) ; if ( value === undefined ) return ; const meta = await this . meta < Meta > ( filePath ) ; return { value : viewToArray ( value ) , expiration : meta . expiration , metadata : meta . metadata , } ; } catch ( e ) { if ( e . code === "ENOTDIR" ) return ; throw e ; } } async put < Meta = unknown > ( key , { value , expiration , metadata } ) { const [ filePath , sanitised ] = this . keyPath ( key ) ; if ( ! filePath ) { throw new FileStorageError ( "ERR_TRAVERSAL" , "Cannot store values outside of storage root directory" ) ; } try { await writeFile ( filePath , value ) ; } catch ( e ) { if ( e . code !== "EEXIST" ) throw e ; throw new FileStorageError ( "ERR_NAMESPACE_KEY_CHILD" , 'Cannot put key "' + key + '" as a parent namespace is also a key.\n' + "This is a limitation of Miniflare's file-system storage. Please " + "use in-memory/Redis storage instead, or change your key layout." , e ) ; } const metaFilePath = filePath + metaSuffix ; if ( expiration !== undefined || metadata !== undefined || sanitised ) { await writeFile ( metaFilePath , JSON . stringify ( { key , expiration , metadata } as FileMeta < Meta > ) ) ; } else { await deleteFile ( metaFilePath ) ; } } async deleteMaybeExpired ( key ) { const [ filePath ] = this . keyPath ( key ) ; if ( ! filePath ) return false ; try { const existed = await deleteFile ( filePath ) ; await deleteFile ( filePath + metaSuffix ) ; return existed ; } catch ( e ) { if ( e . code === "ENOTDIR" ) return false ; throw e ; } } async listAllMaybeExpired < Meta > ( ) { const keys = [ ] ; for await ( const filePath of walk ( this . root ) ) { if ( filePath . endsWith ( metaSuffix ) ) continue ; const name = filePath . substring ( this . root . length + 1 ) ; const meta = await this . meta < Meta > ( filePath ) ; const realName = meta ?. key ?? name ; keys . push ( { name : realName , expiration : meta . expiration , metadata : meta . metadata , } ) ; } return keys ; } }