@lde/distribution-downloader 0.7.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.
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAO5C,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,SAAyD,EAC/D,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC;YA0EZ,mBAAmB;CAyBlC"}
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 { dirname, join, resolve, sep } from 'node:path';
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, filenamifyUrl(distribution.accessUrl)), options) {
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.7.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.8.0",
27
+ "@lde/dataset": "^0.10.0",
28
28
  "filenamify-url": "4.0.0",
29
29
  "tslib": "^2.3.0"
30
30
  }