@netlify/zip-it-and-ship-it 16.1.0 → 16.2.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/main.d.ts +4 -3
- package/dist/main.js +2 -1
- package/dist/manifest.d.ts +23 -5
- package/dist/manifest.js +12 -3
- package/dist/server.d.ts +5 -6
- package/dist/server.js +42 -12
- package/dist/zip.d.ts +7 -37
- package/dist/zip.js +22 -43
- package/package.json +2 -2
package/dist/main.d.ts
CHANGED
|
@@ -5,8 +5,9 @@ import { ModuleFormat } from './runtimes/node/utils/module_format.js';
|
|
|
5
5
|
import { RuntimeName } from './runtimes/runtime.js';
|
|
6
6
|
import type { ExtendedRoute, Route } from './utils/routes.js';
|
|
7
7
|
export { Config, FunctionConfig } from './config.js';
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
8
|
+
export { zipFunction, zipFunctions, zipServer, ZipFunctionOptions, ZipFunctionsOptions } from './zip.js';
|
|
9
|
+
export type { ServerOptions, ServerResult, ZipServerOptions } from './zip.js';
|
|
10
|
+
export { findServerEntry } from './server.js';
|
|
10
11
|
export type { FunctionsBag, MixedPaths } from './paths.js';
|
|
11
12
|
export { ArchiveFormat, ARCHIVE_FORMAT } from './archive.js';
|
|
12
13
|
export type { TrafficRules } from './rate_limit.js';
|
|
@@ -14,7 +15,7 @@ export type { ExtendedRoute, Route } from './utils/routes.js';
|
|
|
14
15
|
export { NodeBundlerName, NODE_BUNDLER } from './runtimes/node/bundlers/types.js';
|
|
15
16
|
export { RuntimeName, RUNTIME } from './runtimes/runtime.js';
|
|
16
17
|
export { ModuleFormat, MODULE_FORMAT } from './runtimes/node/utils/module_format.js';
|
|
17
|
-
export { Manifest } from './manifest.js';
|
|
18
|
+
export type { FunctionsManifest, Manifest, ServerManifest } from './manifest.js';
|
|
18
19
|
export { FunctionResult } from './utils/format_result.js';
|
|
19
20
|
export interface ListedFunction {
|
|
20
21
|
name: string;
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,8 @@ import { parseFile } from './runtimes/node/in_source_config/index.js';
|
|
|
6
6
|
import { RUNTIME } from './runtimes/runtime.js';
|
|
7
7
|
import { RuntimeCache } from './utils/cache.js';
|
|
8
8
|
import { listFunctionsDirectories, resolveFunctionsDirectories } from './utils/fs.js';
|
|
9
|
-
export {
|
|
9
|
+
export { zipFunction, zipFunctions, zipServer } from './zip.js';
|
|
10
|
+
export { findServerEntry } from './server.js';
|
|
10
11
|
export { ARCHIVE_FORMAT } from './archive.js';
|
|
11
12
|
export { NODE_BUNDLER } from './runtimes/node/bundlers/types.js';
|
|
12
13
|
export { RUNTIME } from './runtimes/runtime.js';
|
package/dist/manifest.d.ts
CHANGED
|
@@ -37,9 +37,7 @@ interface ManifestServer {
|
|
|
37
37
|
runtimeVersion?: string;
|
|
38
38
|
vcpu?: number;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
functions: ManifestFunction[];
|
|
42
|
-
server?: ManifestServer;
|
|
40
|
+
interface ManifestBase {
|
|
43
41
|
system: {
|
|
44
42
|
arch: string;
|
|
45
43
|
platform: string;
|
|
@@ -47,9 +45,29 @@ export interface Manifest {
|
|
|
47
45
|
timestamp: number;
|
|
48
46
|
version: number;
|
|
49
47
|
}
|
|
50
|
-
export
|
|
48
|
+
export interface FunctionsManifest extends ManifestBase {
|
|
49
|
+
functions: ManifestFunction[];
|
|
50
|
+
/**
|
|
51
|
+
* A server is described by its own manifest now. Nothing writes this any
|
|
52
|
+
* more; it is kept so that readers of manifests written before the split
|
|
53
|
+
* still compile.
|
|
54
|
+
*/
|
|
55
|
+
server?: ManifestServer;
|
|
56
|
+
}
|
|
57
|
+
export interface ServerManifest extends ManifestBase {
|
|
58
|
+
server: ManifestServer;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The functions manifest, under the name it had before the server manifest was
|
|
62
|
+
* introduced, for backwards compatibility.
|
|
63
|
+
*/
|
|
64
|
+
export type Manifest = FunctionsManifest;
|
|
65
|
+
export declare const createFunctionsManifest: ({ functions, path }: {
|
|
51
66
|
functions: FunctionResult[];
|
|
52
67
|
path: string;
|
|
53
|
-
|
|
68
|
+
}) => Promise<void>;
|
|
69
|
+
export declare const createServerManifest: ({ path, server }: {
|
|
70
|
+
path: string;
|
|
71
|
+
server: ServerResult;
|
|
54
72
|
}) => Promise<void>;
|
|
55
73
|
export {};
|
package/dist/manifest.js
CHANGED
|
@@ -2,10 +2,19 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import { arch, platform } from 'process';
|
|
4
4
|
const MANIFEST_VERSION = 1;
|
|
5
|
-
export const
|
|
5
|
+
export const createFunctionsManifest = async ({ functions, path }) => {
|
|
6
|
+
const formattedFunctions = functions.map((func) => formatFunctionForManifest(func));
|
|
6
7
|
const payload = {
|
|
7
|
-
functions:
|
|
8
|
-
|
|
8
|
+
functions: formattedFunctions,
|
|
9
|
+
system: { arch, platform },
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
version: MANIFEST_VERSION,
|
|
12
|
+
};
|
|
13
|
+
await fs.writeFile(path, JSON.stringify(payload));
|
|
14
|
+
};
|
|
15
|
+
export const createServerManifest = async ({ path, server }) => {
|
|
16
|
+
const payload = {
|
|
17
|
+
server: formatServerForManifest(server),
|
|
9
18
|
system: { arch, platform },
|
|
10
19
|
timestamp: Date.now(),
|
|
11
20
|
version: MANIFEST_VERSION,
|
package/dist/server.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { FeatureFlags } from './feature_flags.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type RuntimeName } from './runtimes/runtime.js';
|
|
3
3
|
import type { RuntimeCache } from './utils/cache.js';
|
|
4
4
|
import type { getLogger } from './utils/logger.js';
|
|
5
5
|
import type { ExtendedRoute, Route } from './utils/routes.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* priority or timeout, because it is not invoked per request.
|
|
7
|
+
* Resolves the server entrypoint inside `directory`, or undefined when there is
|
|
8
|
+
* none.
|
|
10
9
|
*/
|
|
10
|
+
export declare const findServerEntry: (directory: string) => Promise<string | undefined>;
|
|
11
11
|
export type ServerResult = {
|
|
12
12
|
bundler?: string;
|
|
13
13
|
excludedRoutes?: Route[];
|
|
@@ -22,8 +22,7 @@ export type ServerResult = {
|
|
|
22
22
|
vcpu?: number;
|
|
23
23
|
};
|
|
24
24
|
/**
|
|
25
|
-
* The deploy's Netlify Server, and how to bundle it.
|
|
26
|
-
* the server, not by the user.
|
|
25
|
+
* The deploy's Netlify Server, and how to bundle it.
|
|
27
26
|
*/
|
|
28
27
|
export interface ServerOptions {
|
|
29
28
|
/**
|
package/dist/server.js
CHANGED
|
@@ -1,42 +1,72 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { extname, join, resolve } from 'path';
|
|
3
3
|
import { ARCHIVE_FORMAT } from './archive.js';
|
|
4
|
+
import { NODE_BUNDLER } from './runtimes/node/bundlers/types.js';
|
|
4
5
|
import { getFunctionFromPath } from './runtimes/index.js';
|
|
6
|
+
import { RUNTIME } from './runtimes/runtime.js';
|
|
7
|
+
import { FunctionBundlingUserError } from './utils/error.js';
|
|
5
8
|
import { formatZipResult } from './utils/format_result.js';
|
|
6
9
|
import { removeUndefined } from './utils/remove_undefined.js';
|
|
7
10
|
import { getRoutes } from './utils/routes.js';
|
|
8
11
|
// A server claims every path the rest of the deploy does not.
|
|
9
12
|
const SERVER_ROUTE = '/*';
|
|
10
|
-
// Names the server's
|
|
13
|
+
// Names the server's archive. Everywhere else it is only a label, identifying
|
|
14
|
+
// the server in bundling errors, since a server has no name of its own.
|
|
11
15
|
const SERVER_NAME = 'server';
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
const SERVER_ENTRY_BASENAMES = new Set(['index.js', 'index.mjs', 'index.ts', 'index.mts']);
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the server entrypoint inside `directory`, or undefined when there is
|
|
19
|
+
* none.
|
|
20
|
+
*/
|
|
21
|
+
export const findServerEntry = async (directory) => {
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await fs.readdir(directory);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const { code } = error;
|
|
28
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const candidates = entries.filter((name) => SERVER_ENTRY_BASENAMES.has(name)).sort();
|
|
34
|
+
if (candidates.length === 0) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (candidates.length > 1) {
|
|
38
|
+
throw new Error(`Found multiple server entrypoints in ${directory} (${candidates.join(', ')}). A site can have one server only.`);
|
|
39
|
+
}
|
|
40
|
+
return join(directory, candidates[0]);
|
|
41
|
+
};
|
|
14
42
|
const readServerSource = async (srcPath, { cache, featureFlags }) => {
|
|
15
43
|
let source;
|
|
44
|
+
// A server that cannot be read is the user's file being wrong, so it is
|
|
45
|
+
// reported as a bundling failure rather than an internal error.
|
|
46
|
+
const unreadable = (detail) => new FunctionBundlingUserError(`Could not read the Netlify Server at ${srcPath}${detail ? `: ${detail}` : ''}`, {
|
|
47
|
+
functionName: SERVER_NAME,
|
|
48
|
+
runtime: RUNTIME.JAVASCRIPT,
|
|
49
|
+
});
|
|
16
50
|
try {
|
|
17
51
|
source = await getFunctionFromPath(srcPath, { cache, featureFlags });
|
|
18
52
|
}
|
|
19
53
|
catch (error) {
|
|
20
|
-
throw
|
|
54
|
+
throw unreadable(error.message);
|
|
21
55
|
}
|
|
22
56
|
if (source === undefined) {
|
|
23
|
-
throw
|
|
57
|
+
throw unreadable();
|
|
24
58
|
}
|
|
25
59
|
return source;
|
|
26
60
|
};
|
|
27
|
-
// Bundles a Netlify Server through the same runtime as a function, into its own
|
|
28
|
-
// folder so its archive does not land among theirs.
|
|
29
61
|
export const bundleServer = async (server, destFolder, { basePath, cache, featureFlags, logger, repositoryRoot, }) => {
|
|
30
62
|
const srcPath = resolve(server.path);
|
|
31
63
|
const source = await readServerSource(srcPath, { cache, featureFlags });
|
|
32
|
-
const serverFolder = join(destFolder, SERVER_NAME);
|
|
33
|
-
await fs.mkdir(serverFolder, { recursive: true });
|
|
34
64
|
const zipResult = await source.runtime.zipFunction({
|
|
35
65
|
archiveFormat: ARCHIVE_FORMAT.ZIP,
|
|
36
66
|
basePath,
|
|
37
67
|
cache,
|
|
38
|
-
config: source.config,
|
|
39
|
-
destFolder
|
|
68
|
+
config: { ...source.config, nodeBundler: NODE_BUNDLER.NFT },
|
|
69
|
+
destFolder,
|
|
40
70
|
extension: source.extension,
|
|
41
71
|
featureFlags,
|
|
42
72
|
filename: source.filename,
|
|
@@ -51,7 +81,7 @@ export const bundleServer = async (server, destFolder, { basePath, cache, featur
|
|
|
51
81
|
srcPath,
|
|
52
82
|
stat: source.stat,
|
|
53
83
|
});
|
|
54
|
-
const path = join(
|
|
84
|
+
const path = join(destFolder, `${SERVER_NAME}${extname(zipResult.path)}`);
|
|
55
85
|
await fs.rename(zipResult.path, path);
|
|
56
86
|
const result = formatZipResult({
|
|
57
87
|
...zipResult,
|
package/dist/zip.d.ts
CHANGED
|
@@ -17,53 +17,23 @@ export interface ZipFunctionOptions {
|
|
|
17
17
|
debug?: boolean;
|
|
18
18
|
internalSrcFolder?: string;
|
|
19
19
|
}
|
|
20
|
-
export interface BundleResult {
|
|
21
|
-
/**
|
|
22
|
-
* The bundled functions. A server is never among them.
|
|
23
|
-
*/
|
|
24
|
-
functions: FunctionResult[];
|
|
25
|
-
/**
|
|
26
|
-
* The bundled Netlify Server, when one was given.
|
|
27
|
-
*/
|
|
28
|
-
server?: ServerResult;
|
|
29
|
-
}
|
|
30
20
|
export type ZipFunctionsOptions = ZipFunctionOptions & {
|
|
31
21
|
configFileDirectories?: string[];
|
|
32
22
|
manifest?: string;
|
|
33
23
|
parallelLimit?: number;
|
|
34
24
|
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Directories to search, specific function paths, or both.
|
|
41
|
-
*/
|
|
42
|
-
paths: MixedPaths;
|
|
43
|
-
archiveFormat?: ArchiveFormat;
|
|
44
|
-
config?: Config;
|
|
45
|
-
configFileDirectories?: string[];
|
|
46
|
-
parallelLimit?: number;
|
|
47
|
-
}
|
|
48
|
-
export type { ServerOptions, ServerResult };
|
|
49
|
-
export interface BundleOptions {
|
|
50
|
-
/**
|
|
51
|
-
* Where the archives and the manifest are written.
|
|
52
|
-
*/
|
|
53
|
-
destFolder: string;
|
|
54
|
-
functions?: FunctionsOptions;
|
|
55
|
-
server?: ServerOptions;
|
|
25
|
+
export interface ZipServerOptions {
|
|
26
|
+
basePath?: string;
|
|
27
|
+
debug?: boolean;
|
|
28
|
+
featureFlags?: FeatureFlags;
|
|
56
29
|
/**
|
|
57
30
|
* Path of the manifest file. Defaults to `manifest.json` inside `destFolder`.
|
|
58
31
|
*/
|
|
59
32
|
manifest?: string;
|
|
60
|
-
basePath?: string;
|
|
61
|
-
branch?: string;
|
|
62
|
-
debug?: boolean;
|
|
63
|
-
featureFlags?: FeatureFlags;
|
|
64
33
|
repositoryRoot?: string;
|
|
65
34
|
systemLog?: LogFunction;
|
|
66
35
|
}
|
|
67
|
-
export
|
|
68
|
-
export declare const
|
|
36
|
+
export type { ServerOptions, ServerResult };
|
|
37
|
+
export declare const zipFunctions: (input: MixedPaths, destFolder: string, { archiveFormat, basePath, branch, config, configFileDirectories, featureFlags: inputFeatureFlags, manifest, parallelLimit, repositoryRoot, systemLog, debug, }?: ZipFunctionsOptions) => Promise<FunctionResult[]>;
|
|
38
|
+
export declare const zipServer: (srcPath: string, destFolder: string, { basePath, debug, featureFlags: inputFeatureFlags, manifest, repositoryRoot, systemLog, }?: ZipServerOptions) => Promise<ServerResult>;
|
|
69
39
|
export declare const zipFunction: (relativeSrcPath: string, destFolder: string, { archiveFormat, basePath, config: inputConfig, featureFlags: inputFeatureFlags, repositoryRoot, systemLog, debug, internalSrcFolder, }?: ZipFunctionOptions) => Promise<FunctionResult | undefined>;
|
package/dist/zip.js
CHANGED
|
@@ -4,7 +4,7 @@ import isPathInside from 'is-path-inside';
|
|
|
4
4
|
import pMap from 'p-map';
|
|
5
5
|
import { ARCHIVE_FORMAT } from './archive.js';
|
|
6
6
|
import { getFlags } from './feature_flags.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createFunctionsManifest, createServerManifest } from './manifest.js';
|
|
8
8
|
import { bundleServer } from './server.js';
|
|
9
9
|
import { getFunctionsFromPaths } from './runtimes/index.js';
|
|
10
10
|
import { MODULE_FORMAT } from './runtimes/node/utils/module_format.js';
|
|
@@ -23,41 +23,15 @@ const validateArchiveFormat = (archiveFormat) => {
|
|
|
23
23
|
};
|
|
24
24
|
// Zip `srcFolder/*` (Node.js or Go files) to `destFolder/*.zip` so it can be
|
|
25
25
|
// used by AWS Lambda
|
|
26
|
-
export const zipFunctions = async function (input, destFolder, { archiveFormat, basePath, branch, config, configFileDirectories,
|
|
27
|
-
const { functions } = await bundle({
|
|
28
|
-
basePath,
|
|
29
|
-
branch,
|
|
30
|
-
debug,
|
|
31
|
-
destFolder,
|
|
32
|
-
featureFlags,
|
|
33
|
-
functions: {
|
|
34
|
-
archiveFormat,
|
|
35
|
-
config,
|
|
36
|
-
configFileDirectories,
|
|
37
|
-
parallelLimit,
|
|
38
|
-
paths: input,
|
|
39
|
-
},
|
|
40
|
-
manifest,
|
|
41
|
-
repositoryRoot,
|
|
42
|
-
systemLog,
|
|
43
|
-
});
|
|
44
|
-
return functions;
|
|
45
|
-
};
|
|
46
|
-
// Bundles a deploy's functions and its Netlify Server, writing the one manifest
|
|
47
|
-
// that describes both.
|
|
48
|
-
export const bundle = async function ({ basePath, branch, debug, destFolder, featureFlags: inputFeatureFlags, functions: functionsOptions, manifest, repositoryRoot = basePath, server, systemLog, }) {
|
|
49
|
-
const { archiveFormat = ARCHIVE_FORMAT.ZIP, config = {}, configFileDirectories, parallelLimit = DEFAULT_PARALLEL_LIMIT, paths, } = functionsOptions ?? { paths: [] };
|
|
26
|
+
export const zipFunctions = async function (input, destFolder, { archiveFormat = ARCHIVE_FORMAT.ZIP, basePath, branch, config = {}, configFileDirectories, featureFlags: inputFeatureFlags, manifest, parallelLimit = DEFAULT_PARALLEL_LIMIT, repositoryRoot = basePath, systemLog, debug, } = {}) {
|
|
50
27
|
validateArchiveFormat(archiveFormat);
|
|
51
28
|
const logger = getLogger(systemLog, debug);
|
|
52
29
|
const cache = new RuntimeCache();
|
|
53
30
|
const featureFlags = getFlags(inputFeatureFlags);
|
|
54
|
-
const bag = getFunctionsBag(
|
|
31
|
+
const bag = getFunctionsBag(input);
|
|
55
32
|
const srcFolders = resolveFunctionsDirectories([...bag.generated.directories, ...bag.user.directories]);
|
|
56
|
-
const [
|
|
57
|
-
|
|
58
|
-
fs.mkdir(destFolder, { recursive: true }),
|
|
59
|
-
]);
|
|
60
|
-
const functions = await getFunctionsFromPaths([...foundPaths, ...bag.generated.functions, ...bag.user.functions], {
|
|
33
|
+
const [paths] = await Promise.all([listFunctionsDirectories(srcFolders), fs.mkdir(destFolder, { recursive: true })]);
|
|
34
|
+
const functions = await getFunctionsFromPaths([...paths, ...bag.generated.functions, ...bag.user.functions], {
|
|
61
35
|
cache,
|
|
62
36
|
config,
|
|
63
37
|
configFileDirectories,
|
|
@@ -101,21 +75,26 @@ export const bundle = async function ({ basePath, branch, debug, destFolder, fea
|
|
|
101
75
|
const resultWithSize = await addArchiveSize(result);
|
|
102
76
|
return formatZipResult(resultWithSize);
|
|
103
77
|
}));
|
|
104
|
-
|
|
105
|
-
? undefined
|
|
106
|
-
: await bundleServer(server, destFolder, {
|
|
107
|
-
basePath,
|
|
108
|
-
cache,
|
|
109
|
-
featureFlags,
|
|
110
|
-
logger,
|
|
111
|
-
repositoryRoot,
|
|
112
|
-
});
|
|
113
|
-
await createManifest({
|
|
78
|
+
await createFunctionsManifest({
|
|
114
79
|
functions: formattedResults,
|
|
115
80
|
path: resolve(manifest || join(destFolder, 'manifest.json')),
|
|
116
|
-
server: serverResult,
|
|
117
81
|
});
|
|
118
|
-
return
|
|
82
|
+
return formattedResults;
|
|
83
|
+
};
|
|
84
|
+
export const zipServer = async function (srcPath, destFolder, { basePath, debug, featureFlags: inputFeatureFlags, manifest, repositoryRoot = basePath, systemLog, } = {}) {
|
|
85
|
+
const logger = getLogger(systemLog, debug);
|
|
86
|
+
const cache = new RuntimeCache();
|
|
87
|
+
const featureFlags = getFlags(inputFeatureFlags);
|
|
88
|
+
await fs.mkdir(destFolder, { recursive: true });
|
|
89
|
+
const result = await bundleServer({ path: srcPath }, destFolder, {
|
|
90
|
+
basePath,
|
|
91
|
+
cache,
|
|
92
|
+
featureFlags,
|
|
93
|
+
logger,
|
|
94
|
+
repositoryRoot,
|
|
95
|
+
});
|
|
96
|
+
await createServerManifest({ path: resolve(manifest || join(destFolder, 'manifest.json')), server: result });
|
|
97
|
+
return result;
|
|
119
98
|
};
|
|
120
99
|
export const zipFunction = async function (relativeSrcPath, destFolder, { archiveFormat = ARCHIVE_FORMAT.ZIP, basePath, config: inputConfig = {}, featureFlags: inputFeatureFlags, repositoryRoot = basePath, systemLog, debug, internalSrcFolder, } = {}) {
|
|
121
100
|
validateArchiveFormat(archiveFormat);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/zip-it-and-ship-it",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.2.0",
|
|
4
4
|
"description": "Zip it and ship it",
|
|
5
5
|
"main": "./dist/main.js",
|
|
6
6
|
"type": "module",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"engines": {
|
|
101
101
|
"node": ">=22.12.0"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "b1c3e6b1ed61a7be7bc06e4a8e4b2e48f5882de3"
|
|
104
104
|
}
|