Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions components/SourceOptions/FlatFilesystem.vue
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions i18n/locales/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions pages/admin/library/sources/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -257,7 +257,10 @@
* between 'create' and 'edit'
*/

import { SourceOptionsFilesystem } from "#components";
import {
SourceOptionsFilesystem,
SourceOptionsFlatFilesystem,
} from "#components";
import {
DialogTitle,
RadioGroup,
Expand Down Expand Up @@ -295,6 +298,7 @@ const modalLoading = ref(false);

const optionUIs: { [key in LibraryBackend]: Component } = {
Filesystem: SourceOptionsFilesystem,
FlatFilesystem: SourceOptionsFlatFilesystem,
};
const optionsMetadata: {
[key in LibraryBackend]: {
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "LibraryBackend" ADD VALUE 'FlatFilesystem';
3 changes: 2 additions & 1 deletion prisma/models/app.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum Platform {

enum LibraryBackend {
Filesystem
FlatFilesystem
}

model Library {
Expand All @@ -25,5 +26,5 @@ model Library {
backend LibraryBackend
options Json

games Game[]
games Game[]
}
3 changes: 1 addition & 2 deletions server/internal/library/provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Readable } from "stream";
import type { LibraryBackend } from "~/prisma/client";

export abstract class LibraryProvider<CFG> {
Expand Down Expand Up @@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 };
}
}
109 changes: 109 additions & 0 deletions server/internal/library/providers/flat.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
11 changes: 9 additions & 2 deletions server/plugins/05.library-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]: (
Expand All @@ -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 () => {
Expand Down
Loading