@lde/distribution-downloader 0.8.0 → 0.9.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/dist/download.d.ts +12 -0
- package/dist/download.d.ts.map +1 -1
- package/dist/download.js +67 -4
- package/package.json +2 -2
package/dist/download.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export declare class LastModifiedDownloader implements Downloader {
|
|
|
32
32
|
private readonly path;
|
|
33
33
|
constructor(path?: string);
|
|
34
34
|
download(distribution: Distribution, target?: string, options?: DownloadOptions): Promise<DownloadResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Copy a local file into the base directory, because that is the only
|
|
37
|
+
* directory a containerized store has mounted. An unchanged copy is kept: it
|
|
38
|
+
* is what keeps a store’s index of it valid.
|
|
39
|
+
*
|
|
40
|
+
* The copy is unchanged when the source still has the size and modification
|
|
41
|
+
* time it had when copied, as recorded beside the copy. Comparing with the
|
|
42
|
+
* copy’s own time would miss a replacement restored with an older time. The
|
|
43
|
+
* copy keeps the time of copying, so a store indexing by modification time
|
|
44
|
+
* sees it as new.
|
|
45
|
+
*/
|
|
46
|
+
private copyLocalFile;
|
|
35
47
|
private localFileIsUpToDate;
|
|
36
48
|
}
|
|
37
49
|
//# sourceMappingURL=download.d.ts.map
|
package/dist/download.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAQ5C,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC7C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC5C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC9C;AAaD,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CACN,YAAY,EAAE,YAAY,EAC1B,MAAM,CAAC,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5B;AAED,qBAAa,sBAAuB,YAAW,UAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,SAAY;IAEhC,QAAQ,CACnB,YAAY,EAAE,YAAY,EAC1B,MAAM,SAKL,EACD,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC;IAmF1B;;;;;;;;;;OAUG;YACW,aAAa;YAwCb,mBAAmB;CAyBlC"}
|
package/dist/download.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import filenamifyUrl from 'filenamify-url';
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
4
|
import { Transform } from 'node:stream';
|
|
4
5
|
import { pipeline } from 'node:stream/promises';
|
|
5
|
-
import { createWriteStream } from 'node:fs';
|
|
6
|
-
import { access, mkdir, rm, stat } from 'node:fs/promises';
|
|
6
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
7
|
+
import { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
7
8
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
8
9
|
const noop = () => { };
|
|
9
10
|
const noopLogger = {
|
|
@@ -19,7 +20,9 @@ export class LastModifiedDownloader {
|
|
|
19
20
|
constructor(path = 'imports') {
|
|
20
21
|
this.path = path;
|
|
21
22
|
}
|
|
22
|
-
async download(distribution, target = join(this.path,
|
|
23
|
+
async download(distribution, target = join(this.path, distribution.localPath === undefined
|
|
24
|
+
? filenamifyUrl(distribution.accessUrl)
|
|
25
|
+
: localCopyName(distribution.localPath)), options) {
|
|
23
26
|
const logger = options?.logger ?? noopLogger;
|
|
24
27
|
const downloadUrl = distribution.accessUrl;
|
|
25
28
|
const filePath = resolve(target);
|
|
@@ -27,6 +30,10 @@ export class LastModifiedDownloader {
|
|
|
27
30
|
if (!filePath.startsWith(baseDir + sep)) {
|
|
28
31
|
throw new Error(`Download target escapes the base directory: ${filePath}`);
|
|
29
32
|
}
|
|
33
|
+
if (distribution.localPath !== undefined) {
|
|
34
|
+
await this.copyLocalFile(distribution.localPath, filePath, options?.signal);
|
|
35
|
+
return { path: filePath, headers: new Headers() };
|
|
36
|
+
}
|
|
30
37
|
if (await this.localFileIsUpToDate(filePath, distribution)) {
|
|
31
38
|
logger.debug(`File ${filePath} is up to date, skipping download.`);
|
|
32
39
|
return { path: filePath, headers: new Headers() };
|
|
@@ -71,6 +78,50 @@ export class LastModifiedDownloader {
|
|
|
71
78
|
clearTimeout(idleTimer);
|
|
72
79
|
}
|
|
73
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Copy a local file into the base directory, because that is the only
|
|
83
|
+
* directory a containerized store has mounted. An unchanged copy is kept: it
|
|
84
|
+
* is what keeps a store’s index of it valid.
|
|
85
|
+
*
|
|
86
|
+
* The copy is unchanged when the source still has the size and modification
|
|
87
|
+
* time it had when copied, as recorded beside the copy. Comparing with the
|
|
88
|
+
* copy’s own time would miss a replacement restored with an older time. The
|
|
89
|
+
* copy keeps the time of copying, so a store indexing by modification time
|
|
90
|
+
* sees it as new.
|
|
91
|
+
*/
|
|
92
|
+
async copyLocalFile(source, filePath, signal) {
|
|
93
|
+
const sourceStats = await stat(source);
|
|
94
|
+
if (sourceStats.size <= 1) {
|
|
95
|
+
throw new Error(`Local file ${source} is empty`);
|
|
96
|
+
}
|
|
97
|
+
const recordPath = `${filePath}.source.json`;
|
|
98
|
+
const record = JSON.stringify({
|
|
99
|
+
size: sourceStats.size,
|
|
100
|
+
mtimeMs: sourceStats.mtimeMs,
|
|
101
|
+
});
|
|
102
|
+
const [copied, previousRecord] = await Promise.all([
|
|
103
|
+
access(filePath).then(() => true, () => false),
|
|
104
|
+
readFile(recordPath, 'utf8').catch(() => undefined),
|
|
105
|
+
]);
|
|
106
|
+
if (copied && previousRecord === record) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
110
|
+
try {
|
|
111
|
+
// Streamed rather than copyFile, which would carry the source’s mode over:
|
|
112
|
+
// a read-only source would block the next copy, and a private one leave a
|
|
113
|
+
// copy a store running as another user cannot read.
|
|
114
|
+
await pipeline(createReadStream(source), createWriteStream(filePath), {
|
|
115
|
+
signal,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await rm(filePath, { force: true });
|
|
120
|
+
await rm(recordPath, { force: true });
|
|
121
|
+
throw new Error(`Failed to copy ${source} to ${filePath}: ${error}`);
|
|
122
|
+
}
|
|
123
|
+
await writeFile(recordPath, record);
|
|
124
|
+
}
|
|
74
125
|
async localFileIsUpToDate(filePath, distribution) {
|
|
75
126
|
if (undefined === distribution.lastModified) {
|
|
76
127
|
return false;
|
|
@@ -90,3 +141,15 @@ export class LastModifiedDownloader {
|
|
|
90
141
|
return stats.mtime >= distribution.lastModified;
|
|
91
142
|
}
|
|
92
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* A copy’s name, unique per source path: a filename derived from the path would
|
|
146
|
+
* be lossy, so `/rdf/a/b.ttl` and `/rdf/a!b.ttl` would share one copy and one
|
|
147
|
+
* could be indexed as the other. The extensions are kept for the store, which
|
|
148
|
+
* reads the format and compression from them.
|
|
149
|
+
*/
|
|
150
|
+
function localCopyName(path) {
|
|
151
|
+
const hash = createHash('sha256').update(path).digest('hex').slice(0, 16);
|
|
152
|
+
// Always matches, if only the empty string at the end.
|
|
153
|
+
const extensions = /(\.[a-z0-9]+){0,2}$/i.exec(basename(path))[0];
|
|
154
|
+
return `local-${hash}${extensions}`;
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lde/distribution-downloader",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/ldelements/lde.git",
|
|
6
6
|
"directory": "packages/distribution-downloader"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"!**/*.tsbuildinfo"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@lde/dataset": "^0.
|
|
27
|
+
"@lde/dataset": "^0.10.0",
|
|
28
28
|
"filenamify-url": "4.0.0",
|
|
29
29
|
"tslib": "^2.3.0"
|
|
30
30
|
}
|