FlatLibrary provider (#127)

This commit is contained in:
DecDuck
2025-07-06 12:44:41 +10:00
committed by GitHub
parent 73c27f0984
commit 706f2aac83
11 changed files with 233 additions and 75 deletions

View File

@ -0,0 +1,27 @@
<template>
<div>
<label
for="path"
class="block text-sm font-medium leading-6 text-zinc-100"
>{{ $t("library.admin.sources.fsPath") }}</label
>
<p class="text-zinc-400 block text-xs font-medium leading-6">
{{ $t("library.admin.sources.fsPathDesc") }}
</p>
<div class="mt-2">
<input
id="path"
v-model="model!.baseDir"
name="path"
type="text"
autocomplete="path"
:placeholder="$t('library.admin.sources.fsPathPlaceholder')"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
</div>
</template>
<script setup lang="ts">
const model = defineModel<{ baseDir: string }>();
</script>

View File

@ -373,6 +373,7 @@
"fsPath": "Path",
"fsPathDesc": "An absolute path to your game library.",
"fsPathPlaceholder": "/mnt/games",
"fsFlatDesc": "Imports games from a path on disk, but without a separate version subfolder. Useful when migrating an existing library to Drop.",
"link": "Sources {arrow}",
"nameDesc": "The name of your source, for reference.",
"namePlaceholder": "My New Source",

View File

@ -18,7 +18,7 @@
},
"dependencies": {
"@discordapp/twemoji": "^15.1.0",
"@drop-oss/droplet": "^1.3.1",
"@drop-oss/droplet": "1.5.3",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",

View File

@ -167,7 +167,7 @@
'relative block cursor-pointer bg-zinc-800 rounded-lg border border-zinc-900 px-2 py-2 shadow-sm focus:outline-none sm:flex sm:justify-between',
]"
>
<span class="flex items-center gap-x-2">
<span class="flex items-center gap-x-4">
<div>
<component
:is="metadata.icon"
@ -257,7 +257,10 @@
* between 'create' and 'edit'
*/
import { SourceOptionsFilesystem } from "#components";
import {
SourceOptionsFilesystem,
SourceOptionsFlatFilesystem,
} from "#components";
import {
DialogTitle,
RadioGroup,
@ -295,6 +298,7 @@ const modalLoading = ref(false);
const optionUIs: { [key in LibraryBackend]: Component } = {
Filesystem: SourceOptionsFilesystem,
FlatFilesystem: SourceOptionsFlatFilesystem,
};
const optionsMetadata: {
[key in LibraryBackend]: {
@ -306,6 +310,10 @@ const optionsMetadata: {
description: t("library.admin.sources.fsDesc"),
icon: DocumentIcon,
},
FlatFilesystem: {
description: t("library.admin.sources.fsFlatDesc"),
icon: DocumentIcon,
},
};
const optionsMetadataIter = Object.entries(optionsMetadata);

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "LibraryBackend" ADD VALUE 'FlatFilesystem';

View File

@ -16,6 +16,7 @@ enum Platform {
enum LibraryBackend {
Filesystem
FlatFilesystem
}
model Library {

View File

@ -1,4 +1,3 @@
import type { Readable } from "stream";
import type { LibraryBackend } from "~/prisma/client";
export abstract class LibraryProvider<CFG> {
@ -57,7 +56,7 @@ export abstract class LibraryProvider<CFG> {
version: string,
filename: string,
options?: { start?: number; end?: number },
): Promise<Readable | undefined>;
): Promise<ReadableStream | undefined>;
}
export class GameNotFoundError extends Error {}

View File

@ -3,12 +3,11 @@ import {
GameNotFoundError,
VersionNotFoundError,
type LibraryProvider,
} from "./provider";
} from "../provider";
import { LibraryBackend } from "~/prisma/client";
import fs from "fs";
import path from "path";
import droplet from "@drop-oss/droplet";
import type { Readable } from "stream";
export const FilesystemProviderConfig = type({
baseDir: "string",
@ -86,24 +85,29 @@ export class FilesystemProvider
return manifest;
}
// TODO: move this over to the droplet.readfile function it works
async peekFile(game: string, version: string, filename: string) {
const filepath = path.join(this.config.baseDir, game, version);
if (!fs.existsSync(filepath)) return undefined;
const stat = droplet.peekFile(filepath, filename);
return { size: stat };
}
async readFile(
game: string,
version: string,
filename: string,
options?: { start?: number; end?: number },
): Promise<Readable | undefined> {
const filepath = path.join(this.config.baseDir, game, version, filename);
) {
const filepath = path.join(this.config.baseDir, game, version);
if (!fs.existsSync(filepath)) return undefined;
const stream = fs.createReadStream(filepath, options);
const stream = droplet.readFile(
filepath,
filename,
options?.start,
options?.end,
);
if (!stream) return undefined;
return stream;
}
async peekFile(game: string, version: string, filename: string) {
const filepath = path.join(this.config.baseDir, game, version, filename);
if (!fs.existsSync(filepath)) return undefined;
const stat = fs.statSync(filepath);
return { size: stat.size };
}
}

View File

@ -0,0 +1,109 @@
import { ArkErrors, type } from "arktype";
import type { LibraryProvider } from "../provider";
import { VersionNotFoundError } from "../provider";
import { LibraryBackend } from "~/prisma/client";
import fs from "fs";
import path from "path";
import droplet from "@drop-oss/droplet";
export const FlatFilesystemProviderConfig = type({
baseDir: "string",
});
export class FlatFilesystemProvider
implements LibraryProvider<typeof FlatFilesystemProviderConfig.infer>
{
private config: typeof FlatFilesystemProviderConfig.infer;
private myId: string;
constructor(rawConfig: unknown, id: string) {
const config = FlatFilesystemProviderConfig(rawConfig);
if (config instanceof ArkErrors) {
throw new Error(
`Failed to create filesystem provider: ${config.summary}`,
);
}
this.myId = id;
this.config = config;
if (!fs.existsSync(this.config.baseDir))
throw "Base directory does not exist.";
}
type() {
return LibraryBackend.FlatFilesystem;
}
id() {
return this.myId;
}
/**
* These are basically our versions, but also our games.
* @returns list of valid games
*/
async listGames() {
const versionDirs = fs.readdirSync(this.config.baseDir);
const validVersionDirs = versionDirs.filter((e) => {
const fullDir = path.join(this.config.baseDir, e);
return droplet.hasBackendForPath(fullDir);
});
return validVersionDirs;
}
/**
* Doesn't do anything, just returns "default"
* @param _game Ignored
* @returns
*/
async listVersions(_game: string) {
return ["default"];
}
async versionReaddir(game: string, _version: string) {
const versionDir = path.join(this.config.baseDir, game);
if (!fs.existsSync(versionDir)) throw new VersionNotFoundError();
return droplet.listFiles(versionDir);
}
async generateDropletManifest(
game: string,
_version: string,
progress: (err: Error | null, v: number) => void,
log: (err: Error | null, v: string) => void,
) {
const versionDir = path.join(this.config.baseDir, game);
if (!fs.existsSync(versionDir)) throw new VersionNotFoundError();
const manifest = await new Promise<string>((r, j) =>
droplet.generateManifest(versionDir, progress, log, (err, result) => {
if (err) return j(err);
r(result);
}),
);
return manifest;
}
async peekFile(game: string, _version: string, filename: string) {
const filepath = path.join(this.config.baseDir, game);
if (!fs.existsSync(filepath)) return undefined;
const stat = droplet.peekFile(filepath, filename);
return { size: stat };
}
async readFile(
game: string,
_version: string,
filename: string,
options?: { start?: number; end?: number },
) {
const filepath = path.join(this.config.baseDir, game);
if (!fs.existsSync(filepath)) return undefined;
const stream = droplet.readFile(
filepath,
filename,
options?.start,
options?.end,
);
if (!stream) return undefined;
return stream;
}
}

View File

@ -2,10 +2,11 @@ import { LibraryBackend } from "~/prisma/client";
import prisma from "../internal/db/database";
import type { JsonValue } from "@prisma/client/runtime/library";
import type { LibraryProvider } from "../internal/library/provider";
import type { FilesystemProviderConfig } from "../internal/library/filesystem";
import { FilesystemProvider } from "../internal/library/filesystem";
import type { FilesystemProviderConfig } from "../internal/library/providers/filesystem";
import { FilesystemProvider } from "../internal/library/providers/filesystem";
import libraryManager from "../internal/library";
import path from "path";
import { FlatFilesystemProvider } from "../internal/library/providers/flat";
export const libraryConstructors: {
[key in LibraryBackend]: (
@ -19,6 +20,12 @@ export const libraryConstructors: {
): LibraryProvider<unknown> {
return new FilesystemProvider(value, id);
},
FlatFilesystem: function (
value: JsonValue,
id: string,
): LibraryProvider<unknown> {
return new FlatFilesystemProvider(value, id);
},
};
export default defineNitroPlugin(async () => {

108
yarn.lock
View File

@ -330,71 +330,71 @@
jsonfile "^5.0.0"
universalify "^0.1.2"
"@drop-oss/droplet-darwin-arm64@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-arm64/-/droplet-darwin-arm64-1.3.1.tgz#672484c15419dbc068950f2d53f130ccbea3ff17"
integrity sha512-rarsZtIiZhv2hb3bAZSJjxwnme+rWUFY+FY79MRrMnz7EuNBez063pFBqDhwFCz+0QqDBz7zKDUQR6v+6gnVFw==
"@drop-oss/droplet-darwin-arm64@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-arm64/-/droplet-darwin-arm64-1.5.3.tgz#98d34b7b35a9525cf760e17c41d7750f47dab7d1"
integrity sha512-5b7jMv8EgvE0J34dEgl6hitxk5aFtPCxH9xz95Ixz5A1vbqqYlIWWpk4jjBj78bqXQKufHk4ivHPi7btOlwFzQ==
"@drop-oss/droplet-darwin-universal@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-universal/-/droplet-darwin-universal-1.3.1.tgz#0a3e1663125349b2d443dbfaba1bb7b8d2f9c64d"
integrity sha512-PuN5FdotwYuZ7O2r1aAWiE8hH/gH7CrH+j33OdgS4FI4XIOeW6qq+14JECZp6JgWv0863/C7tD5Ll4yMgIRvUQ==
"@drop-oss/droplet-darwin-universal@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-universal/-/droplet-darwin-universal-1.5.3.tgz#3699a7acd958cc6f5a4ef55597dc4d24696133ff"
integrity sha512-IapfmWSEPTw8phdKaKT1Gf7FrHIsWtuCJT8dqtOXycq8ROrZBX3DT5JvhYkHkki0r3DrdOeTkkpy5DoEp2S9gA==
"@drop-oss/droplet-darwin-x64@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-x64/-/droplet-darwin-x64-1.3.1.tgz#755db12988b431eff24df9a7177453178f29ce47"
integrity sha512-IloUIHnEI67S38vJxADbcXk81tR8b4fFTTpyCNUlAwIXHGbhEjFrfu+sLdK94MHN/vjlafMBf0APwYF2vclCkw==
"@drop-oss/droplet-darwin-x64@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-darwin-x64/-/droplet-darwin-x64-1.5.3.tgz#c52de2f746fea9bb9ce8eb70f42ece28fdd7ee08"
integrity sha512-J8w0bBMSwXKYyeGTMkO/4ZmwRWcy5zVpiPQfdu0T3LFfjbMrhVNsFe/g7NyYGGaQKdjpuLIRHl8pS8U5XWWdog==
"@drop-oss/droplet-linux-arm64-gnu@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-arm64-gnu/-/droplet-linux-arm64-gnu-1.3.1.tgz#26ac0b24a08e6785742a19cbf97003f23d3b152d"
integrity sha512-aiesHfQushi+EGmTj970bxZvhNsBh90kzKbg14vdgFTL0/mhcturJSHa0VhJ2/m4qIg10IlmJpbuEm165Q25rQ==
"@drop-oss/droplet-linux-arm64-gnu@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-arm64-gnu/-/droplet-linux-arm64-gnu-1.5.3.tgz#90c1f37be61b12164205cb75ef5fded2a7d4c671"
integrity sha512-eh9Wa9GcV3wu2CjJehnx4y2xjwM2i5vk2uaHCUQa9y6G2bD5pqThtefEwViOfmh7ua6IxepAJ/CE+0eNKy/V3w==
"@drop-oss/droplet-linux-arm64-musl@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-arm64-musl/-/droplet-linux-arm64-musl-1.3.1.tgz#1449cd59a75363a01f2ed68c1921e6d93fbafccd"
integrity sha512-Oa6HvvBbflxoG1nmdYbcgMAf29aTZ6xCxC84X+Q8TjiO5Qx2PHI+nX+KKK8rrJdQszrqpdT9wZbnD4zDtLzSeQ==
"@drop-oss/droplet-linux-arm64-musl@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-arm64-musl/-/droplet-linux-arm64-musl-1.5.3.tgz#7ea5db5a1f05671adffc0560dbbf944825522601"
integrity sha512-1QC80pa2hPrQ/u15pSUhURH2s5Q1Ywz3Dzlr9zQaG+ZCAOoFvGZHTJX//I9gOYmKoKs0CJ5PrqWsdNjGBO9izA==
"@drop-oss/droplet-linux-riscv64-gnu@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-riscv64-gnu/-/droplet-linux-riscv64-gnu-1.3.1.tgz#6300be23128ed4fd8895ca610cc2fb9bdf9abc05"
integrity sha512-vAVUiMixfB/oXIZ7N6QhJB1N+lb96JLrs2EjZiPGNSgwKGMV0H+84ZI+5NJ30qoytm7WB8mm2beezoCpM8frjg==
"@drop-oss/droplet-linux-riscv64-gnu@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-riscv64-gnu/-/droplet-linux-riscv64-gnu-1.5.3.tgz#1052ca2c53f836f487986e77710a3053e45ced38"
integrity sha512-9L8AtfcZOVCtGnFfTfnVJNaA2JhDyVjtsLfXEmE9xqqYjO+JjzHU3KxApDLWuD7vnjv7YbJOn9NXJWF8gbIrXg==
"@drop-oss/droplet-linux-x64-gnu@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-x64-gnu/-/droplet-linux-x64-gnu-1.3.1.tgz#5f59e8816978af444301ad7116a0377f9aa2633a"
integrity sha512-R3UtBIw5amY1ExaX8fZMcS1zLv0DF9Y8YoBgqk+VbQrHMVfiQKiktv/dXRp+9iWzLB/m5aG/Se5QuJazOMlwtA==
"@drop-oss/droplet-linux-x64-gnu@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-x64-gnu/-/droplet-linux-x64-gnu-1.5.3.tgz#237c5fe83305f562817240c8d07edb6934bae598"
integrity sha512-CZUurI+0Sx0PZSVrA/yy7H0jAMWliv6CFgJVQ1KWnALRB39cW7iUX2EOwAWNbxIpT4KpgOuryH+njEbNLJrbrQ==
"@drop-oss/droplet-linux-x64-musl@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-x64-musl/-/droplet-linux-x64-musl-1.3.1.tgz#84b136665dc713c66a3e5b2be6ce5d97514dcb25"
integrity sha512-rDtmTYzx39Y1xHyRvm2AW97GkHy4ZfhXsmYWSjqo0dmoM5BY/nHfmNO6kWOABg4WP6mr3NPZKJTe885JVNilcg==
"@drop-oss/droplet-linux-x64-musl@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-linux-x64-musl/-/droplet-linux-x64-musl-1.5.3.tgz#9f927983aab192dc1659d7d974832638afb04efb"
integrity sha512-Bh+579k2UqEP3WTlMKM5m5cygyx1sSTJo4jVSVkRjjvVJOjrwzwH8s+zm4jlHY/EyTM86QIz31dayg/1gDCY4A==
"@drop-oss/droplet-win32-arm64-msvc@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-win32-arm64-msvc/-/droplet-win32-arm64-msvc-1.3.1.tgz#77948fe25f27dedda979367feadbe89622aa6b19"
integrity sha512-ZQVLgloMd7NIW3j1cvL7DKp9164K8luLxb692yuXRF6pQ7ok8IPWgwiWoeqQ1OE/msPkgXEC7hHupwWtfX6tHw==
"@drop-oss/droplet-win32-arm64-msvc@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-win32-arm64-msvc/-/droplet-win32-arm64-msvc-1.5.3.tgz#80ad0c11f63902b940c8f1809f2840a9cb1069d7"
integrity sha512-aIuLKXKoOmwAWeShZP+LJGP/W5quiaRLoIh3gx53vWdxo1O+CamWvvg0NsbCgo7bEuWiHghl9NAS78Jb/UX2Tw==
"@drop-oss/droplet-win32-x64-msvc@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-win32-x64-msvc/-/droplet-win32-x64-msvc-1.3.1.tgz#35f2dca041af48dec6743bf5c62a35582e25715d"
integrity sha512-NJsZM4g40I0b/MHFTvur3t30ULiU8D3DfhZTlLzyT+btiQ/8PdjCKRM4CPJJhs7JG8Bp30cl2n54XnsnyaFtJA==
"@drop-oss/droplet-win32-x64-msvc@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet-win32-x64-msvc/-/droplet-win32-x64-msvc-1.5.3.tgz#6b21f1c35fce2ed60b836f61b32d0127b98475ee"
integrity sha512-F2XRvlTzKIOKIewjbyBOffguUbsc2Ejga/UJohZJ6t/dBs3NCxcj9QSerxDoeiS470m5kbNWykdaoy+tBhebrg==
"@drop-oss/droplet@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet/-/droplet-1.3.1.tgz#360faadadf50dbe3133ed8aadce6a077d289a48d"
integrity sha512-wXQof5rUiUujAiVwJovAu4Tj2Rhlb0pE/lHSEHF3ywcOLQFcSrE1naQVz3RQ7at+ZkwmDL9fFMXmGJkEKI8jog==
"@drop-oss/droplet@1.5.3":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@drop-oss/droplet/-/droplet-1.5.3.tgz#da06fdaffcbf99458ca3a03b6a5b2fc77f75f7fb"
integrity sha512-e/xNPZ+9gO2jdGlNgEFIFYrbweE8ITAKH9Qdl/lyFsfFOnzfHTUV8OI+8ZewliI84jEhxnMhC+tBC9tAVPR8vg==
optionalDependencies:
"@drop-oss/droplet-darwin-arm64" "1.3.1"
"@drop-oss/droplet-darwin-universal" "1.3.1"
"@drop-oss/droplet-darwin-x64" "1.3.1"
"@drop-oss/droplet-linux-arm64-gnu" "1.3.1"
"@drop-oss/droplet-linux-arm64-musl" "1.3.1"
"@drop-oss/droplet-linux-riscv64-gnu" "1.3.1"
"@drop-oss/droplet-linux-x64-gnu" "1.3.1"
"@drop-oss/droplet-linux-x64-musl" "1.3.1"
"@drop-oss/droplet-win32-arm64-msvc" "1.3.1"
"@drop-oss/droplet-win32-x64-msvc" "1.3.1"
"@drop-oss/droplet-darwin-arm64" "1.5.3"
"@drop-oss/droplet-darwin-universal" "1.5.3"
"@drop-oss/droplet-darwin-x64" "1.5.3"
"@drop-oss/droplet-linux-arm64-gnu" "1.5.3"
"@drop-oss/droplet-linux-arm64-musl" "1.5.3"
"@drop-oss/droplet-linux-riscv64-gnu" "1.5.3"
"@drop-oss/droplet-linux-x64-gnu" "1.5.3"
"@drop-oss/droplet-linux-x64-musl" "1.5.3"
"@drop-oss/droplet-win32-arm64-msvc" "1.5.3"
"@drop-oss/droplet-win32-x64-msvc" "1.5.3"
"@emnapi/core@^1.4.3":
version "1.4.3"