@_linked/server 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pr.yml +18 -0
- package/.github/workflows/publish.yml +15 -145
- package/CHANGELOG.md +21 -0
- package/lib/esm/backend.js +2 -2
- package/lib/esm/backend.js.map +1 -1
- package/lib/esm/shapes/filestores/LocalFileStore.d.ts +21 -2
- package/lib/esm/shapes/filestores/LocalFileStore.js +44 -2
- package/lib/esm/shapes/filestores/LocalFileStore.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +60 -4
- package/.github/workflows/changeset-check.yml +0 -44
- package/.github/workflows/ci.yml +0 -43
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: PR
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
checks:
|
|
10
|
+
uses: linked-fw/.github/.github/workflows/pr.yml@v1
|
|
11
|
+
# The changeset reminder posts a PR comment. A reusable workflow only holds what its
|
|
12
|
+
# caller granted, and the org default is read-only.
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
pull-requests: write
|
|
16
|
+
with:
|
|
17
|
+
require-tests: true
|
|
18
|
+
run-e2e: false
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# Calls the shared workflow in linked-fw/.github.
|
|
2
|
+
#
|
|
3
|
+
# This FILENAME is what npm's trusted-publisher check validates — not the shared workflow's.
|
|
4
|
+
# Renaming this file invalidates this package's trusted publisher, and the only symptom is
|
|
5
|
+
# that releases quietly start needing manual approval again.
|
|
1
6
|
name: Publish
|
|
2
7
|
|
|
3
8
|
on:
|
|
@@ -9,152 +14,17 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
|
9
14
|
|
|
10
15
|
jobs:
|
|
11
16
|
release:
|
|
12
|
-
|
|
13
|
-
runs-on: ubuntu-latest
|
|
17
|
+
uses: linked-fw/.github/.github/workflows/publish.yml@v1
|
|
14
18
|
permissions:
|
|
15
19
|
contents: write
|
|
16
20
|
pull-requests: write
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# the tarball was built, which is worth having on a package a human is about to approve
|
|
21
|
-
# sight-unseen.
|
|
21
|
+
# Needed HERE as well as in the shared workflow: a reusable workflow cannot widen the
|
|
22
|
+
# permissions its caller was granted, and without this npm never gets an id-token and
|
|
23
|
+
# every release silently falls back to the approval queue.
|
|
22
24
|
id-token: write
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
uses: actions/create-github-app-token@v2
|
|
30
|
-
with:
|
|
31
|
-
app-id: ${{ secrets.RELEASE_APP_ID }}
|
|
32
|
-
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
33
|
-
|
|
34
|
-
- name: Checkout
|
|
35
|
-
uses: actions/checkout@v4
|
|
36
|
-
with:
|
|
37
|
-
token: ${{ steps.app-token.outputs.token }}
|
|
38
|
-
|
|
39
|
-
- name: Setup Node.js
|
|
40
|
-
uses: actions/setup-node@v4
|
|
41
|
-
with:
|
|
42
|
-
node-version: 22.16.0
|
|
43
|
-
registry-url: 'https://registry.npmjs.org'
|
|
44
|
-
|
|
45
|
-
# `npm stage` landed in CLI 11.15.0; Node 22.16 ships npm 10.x, so the bundled one
|
|
46
|
-
# cannot stage. Pinned rather than `@latest` so a future npm release cannot change how
|
|
47
|
-
# releases behave without a commit here.
|
|
48
|
-
- name: Use an npm that can stage
|
|
49
|
-
run: npm i -g npm@^11.15.0
|
|
50
|
-
|
|
51
|
-
- name: Install dependencies
|
|
52
|
-
run: npm ci --legacy-peer-deps
|
|
53
|
-
|
|
54
|
-
- name: Build
|
|
55
|
-
run: npm run build
|
|
56
|
-
|
|
57
|
-
# No `publish:` — changesets only opens/updates the version PR here. Publishing is the
|
|
58
|
-
# step below, because `changeset publish` shells out to `npm publish`, which a
|
|
59
|
-
# stage-only token cannot do (changesets/changesets#2025 is open for first-class
|
|
60
|
-
# support). Splitting them keeps changesets doing what it is good at — versions and
|
|
61
|
-
# changelogs — and leaves the registry call to us.
|
|
62
|
-
- name: Create release Pull Request
|
|
63
|
-
id: changesets
|
|
64
|
-
uses: changesets/action@v1
|
|
65
|
-
with:
|
|
66
|
-
title: 'chore: release'
|
|
67
|
-
commit: 'chore: release'
|
|
68
|
-
env:
|
|
69
|
-
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
70
|
-
|
|
71
|
-
# Stage the version for a human to approve.
|
|
72
|
-
#
|
|
73
|
-
# A staged version is uploaded but NOT public: a maintainer reviews it and approves with
|
|
74
|
-
# 2FA, from the Staged Packages tab in the npm user menu or `npm stage approve`. That is
|
|
75
|
-
# the point — every release keeps a proof-of-presence step even though CI is
|
|
76
|
-
# unattended — and it is why the org token is scoped "read and write (stage only)".
|
|
77
|
-
#
|
|
78
|
-
# Idempotent: this workflow runs on every push to main, including the merge of the
|
|
79
|
-
# version PR and any later commit, so it must do nothing when the version is already on
|
|
80
|
-
# the registry or already sitting in the queue.
|
|
81
|
-
- name: Stage the release for approval
|
|
82
|
-
if: steps.changesets.outputs.hasChangesets != 'true'
|
|
83
|
-
env:
|
|
84
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
|
85
|
-
NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
|
86
|
-
run: |
|
|
87
|
-
set -euo pipefail
|
|
88
|
-
name=$(node -p "require('./package.json').name")
|
|
89
|
-
version=$(node -p "require('./package.json').version")
|
|
90
|
-
private=$(node -p "require('./package.json').private === true")
|
|
91
|
-
if [ "$private" = "true" ]; then
|
|
92
|
-
echo "$name is private — nothing to publish."; exit 0
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
if npm view "$name@$version" version >/dev/null 2>&1; then
|
|
96
|
-
echo "$name@$version is already on the registry — nothing to do."
|
|
97
|
-
exit 0
|
|
98
|
-
fi
|
|
99
|
-
|
|
100
|
-
if npm stage list "$name" 2>/dev/null | grep -q "$version"; then
|
|
101
|
-
echo "$name@$version is already staged and waiting for approval."
|
|
102
|
-
exit 0
|
|
103
|
-
fi
|
|
104
|
-
|
|
105
|
-
echo "Staging $name@$version…"
|
|
106
|
-
# Capture the output: `npm stage publish` prints the stage id, and `npm stage
|
|
107
|
-
# approve` takes ONLY that id — a package spec is rejected with "stage-id must be a
|
|
108
|
-
# valid UUID". Without this the notice below hands out a command that does not work.
|
|
109
|
-
out=$(npm stage publish --access public --provenance 2>&1 | tee /dev/stderr)
|
|
110
|
-
uuid='[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
|
111
|
-
# `|| true` because the script runs under `set -euo pipefail`: a grep that matches
|
|
112
|
-
# nothing exits 1 and would fail the step even though the staging itself succeeded.
|
|
113
|
-
stage_id=$(printf '%s' "$out" | grep -oE "$uuid" | tail -1 || true)
|
|
114
|
-
|
|
115
|
-
{
|
|
116
|
-
echo "### 📦 \`$name@$version\` is staged, not published"
|
|
117
|
-
echo
|
|
118
|
-
echo "It is uploaded and held for review. **Approve it to make it public:**"
|
|
119
|
-
echo
|
|
120
|
-
if [ -n "$stage_id" ]; then
|
|
121
|
-
echo "- **CLI:** \`npm stage approve $stage_id\`"
|
|
122
|
-
else
|
|
123
|
-
echo "- **CLI:** \`npm stage list $name\` to find the id, then"
|
|
124
|
-
echo " \`npm stage approve <stage-id>\`"
|
|
125
|
-
fi
|
|
126
|
-
echo "- **Web:** [npmjs.com](https://www.npmjs.com) → your avatar → **Staged"
|
|
127
|
-
echo " Packages** → **Approve**. The button stays disabled until npm has"
|
|
128
|
-
echo " finished scanning the tarball; the page refreshes every minute."
|
|
129
|
-
echo
|
|
130
|
-
echo "Both prompt for 2FA. Until then \`npm install $name\` still resolves"
|
|
131
|
-
echo "\`$(npm view "$name" version 2>/dev/null || echo 'nothing')\`."
|
|
132
|
-
} >> "$GITHUB_STEP_SUMMARY"
|
|
133
|
-
|
|
134
|
-
if [ -n "$stage_id" ]; then
|
|
135
|
-
echo "::notice title=Approval needed::$name@$version is staged. Run: npm stage approve $stage_id"
|
|
136
|
-
else
|
|
137
|
-
echo "::notice title=Approval needed::$name@$version is staged, but the stage id could not be read from the output. Run: npm stage list $name"
|
|
138
|
-
fi
|
|
139
|
-
|
|
140
|
-
# When changesets opened/updated the version PR, merge it so the release reaches the
|
|
141
|
-
# staging queue without a manual click. App token → the merge's push re-triggers this
|
|
142
|
-
# workflow, which then stages. DIRECT-merge (not `--auto`): GitHub
|
|
143
|
-
# auto-merge ignores branch-protection review-bypass allowances; main is check-only so a
|
|
144
|
-
# direct merge lands once the required check is green — retry until then.
|
|
145
|
-
- name: Merge the version PR to stage the release
|
|
146
|
-
if: steps.changesets.outputs.hasChangesets == 'true'
|
|
147
|
-
env:
|
|
148
|
-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
149
|
-
run: |
|
|
150
|
-
pr=$(gh pr list --base main --head changeset-release/main --state open --json number --jq '.[0].number // empty')
|
|
151
|
-
if [ -z "$pr" ]; then echo "No open version PR (nothing to merge)."; exit 0; fi
|
|
152
|
-
for i in $(seq 1 40); do
|
|
153
|
-
if gh pr merge "$pr" --merge 2>/tmp/merge_err; then
|
|
154
|
-
echo "Merged version PR #$pr on attempt $i."
|
|
155
|
-
exit 0
|
|
156
|
-
fi
|
|
157
|
-
echo "Attempt $i/40: version PR #$pr not mergeable yet ($(tr -d '\n' < /tmp/merge_err)); waiting for the required check…"
|
|
158
|
-
sleep 15
|
|
159
|
-
done
|
|
160
|
-
echo "::warning::Version PR #$pr not merged after retries — merge it manually."
|
|
25
|
+
# Named rather than `inherit`: `inherit` forwards everything this repo can see, which is
|
|
26
|
+
# how a community repo would end up forwarding a first-party token.
|
|
27
|
+
secrets:
|
|
28
|
+
NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|
|
29
|
+
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
|
|
30
|
+
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @\_linked/server
|
|
2
2
|
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#38](https://github.com/linked-fw/server/pull/38) [`bf1e83c`](https://github.com/linked-fw/server/commit/bf1e83cdfc949baec88a645c2750479bc33f1637) Thanks [@flyon](https://github.com/flyon)! - `LocalFileStore` accepts the new `SaveFileOptions` third argument of `saveFile`
|
|
8
|
+
(still accepting a positional mime-type string) and gains `statFile()`, which
|
|
9
|
+
returns the size and a sha256 of a stored file, or `null` when it does not
|
|
10
|
+
exist, for verify-after-upload. A local file has no headers, so `cacheControl`
|
|
11
|
+
and `metadata` are accepted and ignored.
|
|
12
|
+
|
|
13
|
+
Core reports an unspecified `preventDuplicates` as `undefined` and applies no
|
|
14
|
+
default of its own, so this store keeps applying its own: a caller that says
|
|
15
|
+
nothing still gets the random file-name suffix, and only an explicit
|
|
16
|
+
`preventDuplicates: false` overwrites an existing name.
|
|
17
|
+
|
|
18
|
+
## 2.3.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- [#40](https://github.com/linked-fw/server/pull/40) [`804851d`](https://github.com/linked-fw/server/commit/804851d16f005b56893c73561aaf72ad64a78434) Thanks [@flyon](https://github.com/flyon)! - Point `repository.url` at the linked-fw organisation, so npm provenance verification matches the repository that builds the package.
|
|
23
|
+
|
|
3
24
|
## 2.3.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/lib/esm/backend.js
CHANGED
|
@@ -7,8 +7,8 @@ import fs from 'fs/promises';
|
|
|
7
7
|
export * from './shapes/quadstores/BackendAPIStoreProvider.js';
|
|
8
8
|
export default class LincdServerBackendProvider extends BackendProvider {
|
|
9
9
|
async setupBeforeControllers() {
|
|
10
|
-
if (!LinkedFileStorage.
|
|
11
|
-
LinkedFileStorage.
|
|
10
|
+
if (!LinkedFileStorage.getDefaultStore()) {
|
|
11
|
+
LinkedFileStorage.setDefaultStore(new LocalFileStore(process.env.NODE_ENV + '-filestore'));
|
|
12
12
|
}
|
|
13
13
|
let fileSystemUploadPath = path.join('data', 'filestores');
|
|
14
14
|
try {
|
package/lib/esm/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,cAAc,gDAAgD,CAAC;AAE/D,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../../src/backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,aAAa,CAAC;AAE7B,cAAc,gDAAgD,CAAC;AAE/D,MAAM,CAAC,OAAO,OAAO,0BAA2B,SAAQ,eAAe;IACrE,KAAK,CAAC,sBAAsB;QAC1B,IAAI,CAAC,iBAAiB,CAAC,eAAe,EAAE,EAAE,CAAC;YACzC,iBAAiB,CAAC,eAAe,CAC/B,IAAI,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC,CACxD,CAAC;QACJ,CAAC;QACD,IAAI,oBAAoB,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACxC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,YAAY,CAAC,EAAE;gBAC5D,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,SAAS;QACP,OAAO,aAAa,EAAE,CAAC;IACzB,CAAC;CACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IFileStore } from '@_linked/core/interfaces/IFileStore';
|
|
1
|
+
import { FileStat, IFileStore, SaveFileOptions } from '@_linked/core/interfaces/IFileStore';
|
|
2
2
|
import { Shape } from '@_linked/core/shapes/Shape';
|
|
3
3
|
export declare class LocalFileStore extends Shape implements IFileStore {
|
|
4
4
|
private readonly basePath;
|
|
@@ -36,7 +36,26 @@ export declare class LocalFileStore extends Shape implements IFileStore {
|
|
|
36
36
|
* Save a file to the local filesystem
|
|
37
37
|
* @param filePath The path to save the file to, relative to the base upload folder
|
|
38
38
|
* @param fileContent The contents of the file as a buffer
|
|
39
|
+
* @param options Save options, or a string read as the old positional mimeType.
|
|
40
|
+
* A local file has no headers to attach, so only `mimeType` (used to add a
|
|
41
|
+
* missing extension) and `preventDuplicates` mean anything here;
|
|
42
|
+
* `cacheControl` and `metadata` are accepted and ignored.
|
|
43
|
+
* @param preventDuplicates Only honoured when `options` is a string
|
|
39
44
|
* @returns A promise that resolves to the public URL of the file
|
|
40
45
|
*/
|
|
41
|
-
saveFile(filePath: string, fileContent: Buffer,
|
|
46
|
+
saveFile(filePath: string, fileContent: Buffer, options?: SaveFileOptions | string, preventDuplicates?: boolean): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Read back the metadata of a stored file, for verify-after-upload.
|
|
49
|
+
*
|
|
50
|
+
* The path is resolved exactly like getFile/fileExists/deleteFile do, against
|
|
51
|
+
* the base upload folder, which by default is the same folder saveFile writes
|
|
52
|
+
* into, so a file that was just saved can be stat'ed by its stored name.
|
|
53
|
+
*
|
|
54
|
+
* The sha256 is computed on demand from the bytes: statFile is only called
|
|
55
|
+
* during publish verification, not on every upload.
|
|
56
|
+
*
|
|
57
|
+
* @param filePath The path to the file, relative to the base upload folder
|
|
58
|
+
* @returns The size and sha256 of the file, or null if it does not exist
|
|
59
|
+
*/
|
|
60
|
+
statFile(filePath: string): Promise<FileStat | null>;
|
|
42
61
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { relativeFileSystemUploadPath, getUploadTarget, } from '@_linked/server-utils/utils/Upload';
|
|
2
|
+
import { normalizeSaveFileOptions, } from '@_linked/core/interfaces/IFileStore';
|
|
2
3
|
import { Shape } from '@_linked/core/shapes/Shape';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
3
5
|
import * as fsSync from 'node:fs';
|
|
4
6
|
import fs from 'node:fs/promises';
|
|
5
7
|
import path from 'node:path';
|
|
@@ -78,10 +80,21 @@ export class LocalFileStore extends Shape {
|
|
|
78
80
|
* Save a file to the local filesystem
|
|
79
81
|
* @param filePath The path to save the file to, relative to the base upload folder
|
|
80
82
|
* @param fileContent The contents of the file as a buffer
|
|
83
|
+
* @param options Save options, or a string read as the old positional mimeType.
|
|
84
|
+
* A local file has no headers to attach, so only `mimeType` (used to add a
|
|
85
|
+
* missing extension) and `preventDuplicates` mean anything here;
|
|
86
|
+
* `cacheControl` and `metadata` are accepted and ignored.
|
|
87
|
+
* @param preventDuplicates Only honoured when `options` is a string
|
|
81
88
|
* @returns A promise that resolves to the public URL of the file
|
|
82
89
|
*/
|
|
83
|
-
async saveFile(filePath, fileContent,
|
|
84
|
-
|
|
90
|
+
async saveFile(filePath, fileContent, options, preventDuplicates) {
|
|
91
|
+
var _a;
|
|
92
|
+
const normalized = normalizeSaveFileOptions(options, preventDuplicates);
|
|
93
|
+
// Core reports an unspecified preventDuplicates as undefined, leaving the
|
|
94
|
+
// default to each store. This one has always let getUploadTarget append its
|
|
95
|
+
// random suffix, so only an explicit `false` turns that off.
|
|
96
|
+
const suffixDuplicates = (_a = normalized.preventDuplicates) !== null && _a !== void 0 ? _a : true;
|
|
97
|
+
const { publicURL, targetFilePath } = getUploadTarget(filePath, normalized.mimeType, null, '', this.accessURL, suffixDuplicates);
|
|
85
98
|
//make sure the target folder exists
|
|
86
99
|
if (!fsSync.existsSync(path.dirname(targetFilePath))) {
|
|
87
100
|
fsSync.mkdirSync(path.dirname(targetFilePath), { recursive: true });
|
|
@@ -89,5 +102,34 @@ export class LocalFileStore extends Shape {
|
|
|
89
102
|
await fs.writeFile(targetFilePath, fileContent);
|
|
90
103
|
return publicURL;
|
|
91
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Read back the metadata of a stored file, for verify-after-upload.
|
|
107
|
+
*
|
|
108
|
+
* The path is resolved exactly like getFile/fileExists/deleteFile do, against
|
|
109
|
+
* the base upload folder, which by default is the same folder saveFile writes
|
|
110
|
+
* into, so a file that was just saved can be stat'ed by its stored name.
|
|
111
|
+
*
|
|
112
|
+
* The sha256 is computed on demand from the bytes: statFile is only called
|
|
113
|
+
* during publish verification, not on every upload.
|
|
114
|
+
*
|
|
115
|
+
* @param filePath The path to the file, relative to the base upload folder
|
|
116
|
+
* @returns The size and sha256 of the file, or null if it does not exist
|
|
117
|
+
*/
|
|
118
|
+
async statFile(filePath) {
|
|
119
|
+
const fileToStat = path.join(this.basePath, filePath);
|
|
120
|
+
let stat;
|
|
121
|
+
try {
|
|
122
|
+
stat = await fs.stat(fileToStat);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (!stat.isFile()) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const contents = await fs.readFile(fileToStat);
|
|
131
|
+
const sha256 = createHash('sha256').update(contents).digest('hex');
|
|
132
|
+
return { size: stat.size, sha256 };
|
|
133
|
+
}
|
|
92
134
|
}
|
|
93
135
|
//# sourceMappingURL=LocalFileStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalFileStore.js","sourceRoot":"","sources":["../../../../src/shapes/filestores/LocalFileStore.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,eAAe,GAChB,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"LocalFileStore.js","sourceRoot":"","sources":["../../../../src/shapes/filestores/LocalFileStore.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/linked-
|
|
11
|
+
"url": "git+https://github.com/linked-fw/server.git"
|
|
12
12
|
},
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.4.0",
|
|
14
14
|
"linkedPackage": true,
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "lib/esm/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@_linked/cli": "^1.2",
|
|
38
|
-
"@_linked/core": "^2.
|
|
38
|
+
"@_linked/core": "^2.20.0",
|
|
39
39
|
"@_linked/server-utils": "^1.2.0",
|
|
40
40
|
"adm-zip": "^0.5.16",
|
|
41
41
|
"archiver": "^7.0.1",
|
package/readme.md
CHANGED
|
@@ -319,8 +319,8 @@ shape.
|
|
|
319
319
|
|
|
320
320
|
### LocalFileStore
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
An `IFileStore` that stores files in a local directory. Its usage follows the general pattern of other file stores in
|
|
323
|
+
LINCD:
|
|
324
324
|
|
|
325
325
|
- Define all storage locations when the server or application starts up
|
|
326
326
|
- Access methods of the file store through the `LinkedFileStorage` class
|
|
@@ -330,15 +330,71 @@ It's important to note that this file store is not suitable for frontend usage -
|
|
|
330
330
|
environment that has access to the [`fs` module](https://nodejs.org/api/fs.html).
|
|
331
331
|
|
|
332
332
|
```ts
|
|
333
|
-
import { LinkedFileStorage } from '@_linked/core/
|
|
334
|
-
import { LocalFileStore } from '
|
|
333
|
+
import { LinkedFileStorage } from '@_linked/core/utils/LinkedFileStorage';
|
|
334
|
+
import { LocalFileStore } from '@_linked/server/shapes/filestores/LocalFileStore';
|
|
335
335
|
import path from 'path';
|
|
336
336
|
|
|
337
|
+
// the second argument is the base folder on disk; it defaults to the server's
|
|
338
|
+
// upload folder (`./data/uploads`, relative to the working directory)
|
|
337
339
|
const pathToStore = path.join(process.cwd(), 'my-file-store');
|
|
338
340
|
const store = new LocalFileStore('my-file-store', pathToStore);
|
|
339
341
|
LinkedFileStorage.setDefaultStore(store);
|
|
340
342
|
```
|
|
341
343
|
|
|
344
|
+
`accessURL` comes from `SITE_ROOT`, and every path you pass to `saveFile`, `getFile`, `fileExists`, `deleteFile` and
|
|
345
|
+
`statFile` is relative to that base folder.
|
|
346
|
+
|
|
347
|
+
Registering stores per purpose (`LinkedFileStorage.setStore`, `getStore`, `registerPurpose`) is core's concern — see
|
|
348
|
+
[`@_linked/core`](https://github.com/linked-cm/core), `utils/LinkedFileStorage` and `interfaces/IFileStore`.
|
|
349
|
+
|
|
350
|
+
#### Saving files
|
|
351
|
+
|
|
352
|
+
`saveFile` takes core's `SaveFileOptions` as its third argument:
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
await store.saveFile('report.pdf', fileBuffer, {
|
|
356
|
+
mimeType: 'application/pdf',
|
|
357
|
+
preventDuplicates: true,
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
A local file has no headers to attach, so only two of the options do anything here:
|
|
362
|
+
|
|
363
|
+
- `mimeType` — used to add a missing file extension.
|
|
364
|
+
- `preventDuplicates` — see below.
|
|
365
|
+
- `cacheControl` and `metadata` are accepted (so the same call works against an S3-backed store) and ignored.
|
|
366
|
+
|
|
367
|
+
The old positional form still works: a plain string third argument is read as the mime type, with `preventDuplicates`
|
|
368
|
+
following it.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
await store.saveFile('report.pdf', fileBuffer, 'application/pdf', true);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
##### LocalFileStore suffixes by default
|
|
375
|
+
|
|
376
|
+
`preventDuplicates` left unspecified means `true` **here**. A second save of the same name does **not** overwrite the
|
|
377
|
+
first: the store appends a random suffix, so `report.pdf` becomes `report_a1b2c3.pdf`. Only an explicit `false`
|
|
378
|
+
overwrites.
|
|
379
|
+
|
|
380
|
+
Core supplies no default of its own — an unspecified `preventDuplicates` reaches the store as `undefined` and each store
|
|
381
|
+
decides. Do not assume this store's choice holds elsewhere: `S3FileStore` in `@_linked/s3` overwrites by default.
|
|
382
|
+
|
|
383
|
+
#### Reading file metadata
|
|
384
|
+
|
|
385
|
+
`statFile(filePath)` returns `{size, sha256}` for a stored file, or `null` when it does not exist (or is not a regular
|
|
386
|
+
file). The `sha256` is a hex digest computed from the file's bytes on demand, so a caller can verify an upload it just
|
|
387
|
+
made:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const stat = await store.statFile('report.pdf');
|
|
391
|
+
if (stat === null) throw new Error('upload went missing');
|
|
392
|
+
if (stat.sha256 !== expectedSha256) throw new Error('content mismatch');
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`statFile` is optional on `IFileStore`, so a caller holding an `IFileStore` rather than a `LocalFileStore` must check
|
|
396
|
+
that it exists before calling it.
|
|
397
|
+
|
|
342
398
|
## TODO
|
|
343
399
|
|
|
344
400
|
- [ ] Add tests
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
name: Changeset Check
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
changeset-check:
|
|
10
|
-
name: Check for changeset
|
|
11
|
-
# Skip the check on the automated Release PR.
|
|
12
|
-
if: ${{ github.head_ref != 'changeset-release/main' }}
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
with:
|
|
18
|
-
fetch-depth: 0
|
|
19
|
-
|
|
20
|
-
- name: Check for changeset files
|
|
21
|
-
uses: actions/github-script@v7
|
|
22
|
-
with:
|
|
23
|
-
script: |
|
|
24
|
-
const { data: files } = await github.rest.pulls.listFiles({
|
|
25
|
-
owner: context.repo.owner,
|
|
26
|
-
repo: context.repo.repo,
|
|
27
|
-
pull_number: context.issue.number,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const hasChangeset = files.some(f =>
|
|
31
|
-
f.filename.startsWith('.changeset/') &&
|
|
32
|
-
f.filename.endsWith('.md') &&
|
|
33
|
-
f.filename !== '.changeset/README.md'
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
if (!hasChangeset) {
|
|
37
|
-
await github.rest.issues.createComment({
|
|
38
|
-
owner: context.repo.owner,
|
|
39
|
-
repo: context.repo.repo,
|
|
40
|
-
issue_number: context.issue.number,
|
|
41
|
-
body: "No changeset found. If this PR should trigger a release, run `npx changeset` and commit the generated file.\n\nIf this change doesn't need a release (docs, CI config, etc.), you can ignore this message."
|
|
42
|
-
});
|
|
43
|
-
core.warning('No changeset file found in this PR.');
|
|
44
|
-
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
build-and-test:
|
|
10
|
-
name: Build & Test
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- name: Detect Release PR
|
|
14
|
-
id: rpr
|
|
15
|
-
run: |
|
|
16
|
-
if [[ "${{ github.event.pull_request.head.ref }}" == "changeset-release/main" ]]; then
|
|
17
|
-
echo "is_release_pr=true" >> $GITHUB_OUTPUT
|
|
18
|
-
echo "Release PR from changesets/action detected — skipping build/test (auto-generated version+CHANGELOG only)."
|
|
19
|
-
else
|
|
20
|
-
echo "is_release_pr=false" >> $GITHUB_OUTPUT
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
- name: Checkout
|
|
24
|
-
if: steps.rpr.outputs.is_release_pr == 'false'
|
|
25
|
-
uses: actions/checkout@v4
|
|
26
|
-
|
|
27
|
-
- name: Setup Node.js
|
|
28
|
-
if: steps.rpr.outputs.is_release_pr == 'false'
|
|
29
|
-
uses: actions/setup-node@v4
|
|
30
|
-
with:
|
|
31
|
-
node-version: 22.16.0
|
|
32
|
-
|
|
33
|
-
- name: Install dependencies
|
|
34
|
-
if: steps.rpr.outputs.is_release_pr == 'false'
|
|
35
|
-
run: npm ci --legacy-peer-deps
|
|
36
|
-
|
|
37
|
-
- name: Build
|
|
38
|
-
if: steps.rpr.outputs.is_release_pr == 'false'
|
|
39
|
-
run: npm run build
|
|
40
|
-
|
|
41
|
-
- name: Test
|
|
42
|
-
if: steps.rpr.outputs.is_release_pr == 'false'
|
|
43
|
-
run: npm test --if-present
|