mirror of
https://github.com/Drop-OSS/drop
synced 2026-08-27 14:23:05 -04:00
Merge f92a13b649 into 3e37863657
This commit is contained in:
commit
7497366f7f
10 changed files with 220 additions and 181 deletions
17
.github/workflows/client-release.yml
vendored
17
.github/workflows/client-release.yml
vendored
|
|
@ -36,32 +36,31 @@ jobs:
|
|||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: pnpm
|
||||
|
||||
|
||||
- name: install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||
targets: ${{ matrix.platform == 'macos-14' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './desktop/src-tauri -> target'
|
||||
workspaces: "./desktop/src-tauri -> target"
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm' # This must match the platform value defined above.
|
||||
|
|
@ -119,7 +118,7 @@ jobs:
|
|||
- name: install frontend dependencies
|
||||
run: pnpm install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
- uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 # tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Do NOT set APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD here. Doing so
|
||||
|
|
@ -137,4 +136,4 @@ jobs:
|
|||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: ${{ matrix.args }}
|
||||
projectPath: './desktop'
|
||||
projectPath: "./desktop"
|
||||
|
|
|
|||
9
.github/workflows/droplet-ci.yml
vendored
9
.github/workflows/droplet-ci.yml
vendored
|
|
@ -20,6 +20,9 @@ on:
|
|||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: Build, Test, Lint
|
||||
|
|
@ -29,15 +32,15 @@ jobs:
|
|||
working-directory: libraries/droplet
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./libraries/droplet -> target"
|
||||
|
||||
|
|
|
|||
14
.github/workflows/pages.yml
vendored
14
.github/workflows/pages.yml
vendored
|
|
@ -31,15 +31,15 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
|
|
@ -52,10 +52,10 @@ jobs:
|
|||
|
||||
- name: Setup Pages
|
||||
id: setup_pages
|
||||
uses: actions/configure-pages@v5
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # actions/configure-pages@v5
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
sites/promo/.next/cache
|
||||
|
|
@ -84,7 +84,7 @@ jobs:
|
|||
cp -r sites/docs/dist/. sites/promo/out/docs/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: sites/promo/out
|
||||
|
||||
|
|
@ -97,4 +97,4 @@ jobs:
|
|||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # actions/deploy-pages@v4
|
||||
|
|
|
|||
22
.github/workflows/server-release.yml
vendored
22
.github/workflows/server-release.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 3 # fix for when this gets triggered by tag
|
||||
fetch-tags: true
|
||||
|
|
@ -41,22 +41,22 @@ jobs:
|
|||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3
|
||||
|
||||
- name: Determine final version
|
||||
id: get_final_ver
|
||||
|
|
@ -78,7 +78,7 @@ jobs:
|
|||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # docker/build-push-action@v6
|
||||
with:
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
|
@ -97,7 +97,7 @@ jobs:
|
|||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
|
|
@ -113,24 +113,24 @@ jobs:
|
|||
contents: read
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # docker/setup-buildx-action@v3
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/drop-OSS/drop
|
||||
|
|
|
|||
6
.github/workflows/torrential-ci.yml
vendored
6
.github/workflows/torrential-ci.yml
vendored
|
|
@ -34,13 +34,13 @@ jobs:
|
|||
working-directory: torrential
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@7c8d7d138f5c09cef361f8214cf96882cd029cdb # dtolnay/rust-toolchain@nightly
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
uses: swatinem/rust-cache@49a0bdc70d2e1b713ca9e2869b211fcce03d3c1c # swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./torrential -> target"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,89 +1,10 @@
|
|||
<template>
|
||||
<!-- go away eslint -->
|
||||
<div />
|
||||
<!-- I don't want to localize this -->
|
||||
<!--
|
||||
<div>
|
||||
<div v-if="user" class="mx-auto max-w-2xl lg:mx-0">
|
||||
<h2
|
||||
class="mt-2 text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
|
||||
>
|
||||
Hello, {{ user.displayName }}!
|
||||
</h2>
|
||||
<p
|
||||
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
|
||||
>
|
||||
Welcome to your Drop account. Here you can view and manage your account
|
||||
information.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="user" class="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 shadow-sm transition-all duration-200 hover:shadow-lg hover:shadow-zinc-900/50"
|
||||
>
|
||||
<div class="p-6">
|
||||
<h3 class="text-base font-semibold text-zinc-100">
|
||||
Account Information
|
||||
</h3>
|
||||
<dl class="mt-4 space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-zinc-400">Username</dt>
|
||||
<dd class="text-sm text-zinc-100">{{ user.username }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-zinc-400">Email</dt>
|
||||
<dd class="text-sm text-zinc-100">{{ user.email }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm font-medium text-zinc-400">Account Type</dt>
|
||||
<dd>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset',
|
||||
user.admin
|
||||
? 'bg-blue-400/10 text-blue-400 ring-blue-400/20'
|
||||
: 'bg-zinc-400/10 text-zinc-400 ring-zinc-400/20',
|
||||
]"
|
||||
>
|
||||
{{ user.admin ? "Administrator" : "Standard User" }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900 shadow-sm transition-all duration-200 hover:shadow-lg hover:shadow-zinc-900/50"
|
||||
>
|
||||
<div class="p-6">
|
||||
<h3 class="text-base font-semibold text-zinc-100">Account Actions</h3>
|
||||
<div class="mt-4 space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full inline-flex items-center justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full inline-flex items-center justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600"
|
||||
>
|
||||
Update Email
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center min-h-[200px]">
|
||||
<div class="text-zinc-400">Loading account information...</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// NOTE: A fuller account-page layout was drafted but is not wired up yet
|
||||
// ("I don't want to localize this"). It lives in git history on this file.
|
||||
definePageMeta({
|
||||
layout: "default",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ export class IGDBProvider implements MetadataProvider {
|
|||
|
||||
const results: GameMetadataSearchResult[] = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
let icon = "";
|
||||
let icon: string;
|
||||
const cover = response[i].cover;
|
||||
if (cover !== undefined) {
|
||||
icon = await this.getIconURL(cover);
|
||||
|
|
@ -547,8 +547,8 @@ export class IGDBProvider implements MetadataProvider {
|
|||
const genres = await this.getGenres(currentGame.genres);
|
||||
const ageRatings = await this.getAgeRatings(currentGame.age_ratings);
|
||||
|
||||
let description = "";
|
||||
let shortDescription = "";
|
||||
let description: string;
|
||||
let shortDescription: string;
|
||||
|
||||
if (currentGame.summary.length > (currentGame.storyline?.length ?? 0)) {
|
||||
description = currentGame.summary;
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ export class MetadataHandler {
|
|||
ReturnType<typeof metadataHandler.fetchCompany>
|
||||
>;
|
||||
} = {};
|
||||
let metadata: GameMetadata | undefined = undefined;
|
||||
let metadata: GameMetadata | undefined;
|
||||
try {
|
||||
metadata = await provider.fetchGame(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -664,17 +664,34 @@ export class SteamProvider implements MetadataProvider {
|
|||
}
|
||||
|
||||
private _decodeHtmlEntities(text: string): string {
|
||||
return text
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16)),
|
||||
)
|
||||
.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
|
||||
// Decode in a single pass so decoded '&' characters cannot combine with
|
||||
// later passes into new entities (double-unescaping).
|
||||
return text.replace(
|
||||
/&(?:nbsp|amp|lt|gt|quot|#39|#x[0-9A-Fa-f]+|#\d+);/g,
|
||||
(entity) => {
|
||||
switch (entity) {
|
||||
case " ":
|
||||
return " ";
|
||||
case "&":
|
||||
return "&";
|
||||
case "<":
|
||||
return "<";
|
||||
case ">":
|
||||
return ">";
|
||||
case """:
|
||||
return '"';
|
||||
case "'":
|
||||
return "'";
|
||||
default: {
|
||||
const hex = entity.match(/&#x([0-9A-Fa-f]+);/);
|
||||
if (hex) return String.fromCharCode(parseInt(hex[1]!, 16));
|
||||
const dec = entity.match(/&#(\d+);/);
|
||||
if (dec) return String.fromCharCode(parseInt(dec[1]!, 10));
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchGameDetails(
|
||||
|
|
@ -908,8 +925,10 @@ export class SteamProvider implements MetadataProvider {
|
|||
}
|
||||
|
||||
private _convertBasicHtmlElements(markdown: string): string {
|
||||
// Remove HTML comments
|
||||
markdown = markdown.replace(/<!--[\s\S]*?-->/g, "");
|
||||
// Neutralize HTML comments by removing every '<!--' opener regardless of
|
||||
// whether a matching '-->' exists, so no fragment of a comment can
|
||||
// survive — including crafted input like "<!--><!-->".
|
||||
markdown = markdown.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
|
||||
|
||||
// Convert the bullet points and tabs to markdown list format
|
||||
markdown = markdown.replace(/•\s*\t+/g, "\n- ");
|
||||
|
|
@ -1102,14 +1121,50 @@ export class SteamProvider implements MetadataProvider {
|
|||
return markdown;
|
||||
}
|
||||
|
||||
private _stripHtmlTags(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
private _stripHtmlTags(input: string): string {
|
||||
// Remove every '<' character first — legitimate markup was already
|
||||
// converted to Markdown earlier in the pipeline, so no tag openers are
|
||||
// expected here. Removing the character itself (rather than matching
|
||||
// whole tags) is complete by construction: with no '<' left in the
|
||||
// string, a sequence like "<script" cannot exist.
|
||||
let html = input;
|
||||
while (html.includes("<")) {
|
||||
html = html.replaceAll("<", "");
|
||||
}
|
||||
|
||||
// Decode entities except </>, which are dropped outright so angle
|
||||
// brackets can never be resurrected. Single-pass replacement prevents
|
||||
// crafted input like "&lt;" from recombining into new entities.
|
||||
// Numeric references encoding 0x3C/0x3E (<, <) are dropped too.
|
||||
return html.replace(
|
||||
/&(?:nbsp|amp|quot|#39|#x[0-9A-Fa-f]+|#\d+);/gi,
|
||||
(entity) => {
|
||||
switch (entity.toLowerCase()) {
|
||||
case " ":
|
||||
return " ";
|
||||
case "&":
|
||||
return "&";
|
||||
case """:
|
||||
return '"';
|
||||
case "'":
|
||||
return "'";
|
||||
default: {
|
||||
const hex = entity.match(/&#x([0-9A-Fa-f]+);/i);
|
||||
if (hex) {
|
||||
const code = parseInt(hex[1]!, 16);
|
||||
if (code === 0x3c || code === 0x3e) return "";
|
||||
return String.fromCharCode(code);
|
||||
}
|
||||
const dec = entity.match(/&#(\d+);/);
|
||||
if (dec) {
|
||||
const code = parseInt(dec[1]!, 10);
|
||||
if (code === 0x3c || code === 0x3e) return "";
|
||||
return String.fromCharCode(code);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,36 +32,75 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
|
||||
async fetch(id: ObjectReference) {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
if (!fs.existsSync(objectPath)) return undefined;
|
||||
return fs.createReadStream(objectPath);
|
||||
// Open the file first and stream from the handle, so there is no window
|
||||
// between checking the file and using it.
|
||||
let handle: fs.promises.FileHandle;
|
||||
try {
|
||||
handle = await fs.promises.open(objectPath, "r");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const stat = await handle.stat();
|
||||
if (!stat.isFile()) {
|
||||
await handle.close();
|
||||
return undefined;
|
||||
}
|
||||
} catch {
|
||||
await handle.close();
|
||||
return undefined;
|
||||
}
|
||||
// createReadStream on the fd keeps reads tied to the opened inode.
|
||||
return fs
|
||||
.createReadStream(undefined as unknown as string, {
|
||||
fd: handle.fd,
|
||||
autoClose: true,
|
||||
})
|
||||
.on("close", () => void handle.close().catch(() => {}));
|
||||
}
|
||||
async write(id: ObjectReference, source: Source): Promise<boolean> {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
if (!fs.existsSync(objectPath)) return false;
|
||||
// Open with 'r+' so we only write to a file that already exists (created
|
||||
// via create()); the check and the write go through the same fd.
|
||||
let handle: fs.promises.FileHandle;
|
||||
try {
|
||||
handle = await fs.promises.open(objectPath, "r+");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove item from cache
|
||||
await this.hashStore.delete(id);
|
||||
|
||||
if (source instanceof Readable) {
|
||||
const outputStream = fs.createWriteStream(objectPath);
|
||||
source.pipe(outputStream, { end: true });
|
||||
await new Promise((r, _j) => source.on("end", r));
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (source instanceof Readable) {
|
||||
const outputStream = handle.createWriteStream();
|
||||
source.pipe(outputStream, { end: true });
|
||||
await new Promise((r, _j) => source.on("end", r));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (source instanceof Buffer) {
|
||||
fs.writeFileSync(objectPath, source);
|
||||
return true;
|
||||
}
|
||||
if (source instanceof Buffer) {
|
||||
await handle.write(source, 0, source.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return false;
|
||||
} finally {
|
||||
await handle.close().catch(() => {});
|
||||
}
|
||||
}
|
||||
async startWriteStream(id: ObjectReference) {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
if (!fs.existsSync(objectPath)) return undefined;
|
||||
// remove item from cache
|
||||
await this.hashStore.delete(id);
|
||||
return fs.createWriteStream(objectPath);
|
||||
// 'r+' fails when the file doesn't exist, replacing the existsSync pre-check.
|
||||
try {
|
||||
const handle = await fs.promises.open(objectPath, "r+");
|
||||
// remove item from cache
|
||||
await this.hashStore.delete(id);
|
||||
return handle.createWriteStream({ autoClose: true });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
async create(
|
||||
id: string,
|
||||
|
|
@ -70,14 +109,18 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
): Promise<ObjectReference | undefined> {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (fs.existsSync(objectPath) || fs.existsSync(metadataPath))
|
||||
|
||||
// Use exclusive-create flags so concurrent creates cannot clobber each
|
||||
// other; failure here means the object already exists.
|
||||
try {
|
||||
// Write metadata
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata), { flag: "wx" });
|
||||
|
||||
// Create file so write passes
|
||||
fs.writeFileSync(objectPath, "", { flag: "wx" });
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
// Write metadata
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
||||
|
||||
// Create file so write passes
|
||||
fs.writeFileSync(objectPath, "");
|
||||
}
|
||||
|
||||
// Call write
|
||||
this.write(id, source);
|
||||
|
|
@ -87,14 +130,17 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
async createWithWriteStream(id: string, metadata: ObjectMetadata) {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (fs.existsSync(objectPath) || fs.existsSync(metadataPath))
|
||||
|
||||
// Exclusive-create flags prevent concurrent creates from clobbering.
|
||||
try {
|
||||
// Write metadata
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata), { flag: "wx" });
|
||||
|
||||
// Create file so write passes
|
||||
fs.writeFileSync(objectPath, "", { flag: "wx" });
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
// Write metadata
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
||||
|
||||
// Create file so write passes
|
||||
fs.writeFileSync(objectPath, "");
|
||||
}
|
||||
|
||||
const stream = await this.startWriteStream(id);
|
||||
if (!stream) throw new Error("Could not create write stream");
|
||||
|
|
@ -102,11 +148,17 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
}
|
||||
async delete(id: ObjectReference): Promise<boolean> {
|
||||
const objectPath = path.join(this.baseObjectPath, id);
|
||||
if (!fs.existsSync(objectPath)) return true;
|
||||
fs.rmSync(objectPath);
|
||||
try {
|
||||
await fs.promises.rm(objectPath);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (!fs.existsSync(metadataPath)) return true;
|
||||
fs.rmSync(metadataPath);
|
||||
try {
|
||||
await fs.promises.rm(metadataPath);
|
||||
} catch {
|
||||
// Metadata may already be gone; nothing to do.
|
||||
}
|
||||
// remove item from caches
|
||||
await this.metadataCache.remove(id);
|
||||
await this.hashStore.delete(id);
|
||||
|
|
@ -119,9 +171,13 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
if (cacheResult !== null) return cacheResult;
|
||||
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (!fs.existsSync(metadataPath)) return undefined;
|
||||
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
||||
const metadata = objectMetadata(metadataRaw);
|
||||
let metadataRaw: string;
|
||||
try {
|
||||
metadataRaw = await fs.promises.readFile(metadataPath, "utf-8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const metadata = objectMetadata(JSON.parse(metadataRaw));
|
||||
if (metadata instanceof type.errors) {
|
||||
logger.error(
|
||||
{ summary: metadata.summary },
|
||||
|
|
@ -137,8 +193,13 @@ export class FsObjectBackend extends ObjectBackend {
|
|||
metadata: ObjectMetadata,
|
||||
): Promise<boolean> {
|
||||
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
|
||||
if (!fs.existsSync(metadataPath)) return false;
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
|
||||
try {
|
||||
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata), {
|
||||
flag: "r+",
|
||||
});
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
await this.metadataCache.set(id, metadata);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue