@netlify/blobs
Advanced tools
Comparing version
@@ -232,2 +232,2 @@ declare global { | ||
export { BlobResponseType, EnvironmentContext, GetDeployStoreOptions, GetStoreOptions, GetWithMetadataOptions, GetWithMetadataResult, ListOptions, ListResultBlob, SetOptions, Store, StoreOptions, connectLambda, getDeployStore, getStore, listStores, setEnvironmentContext }; | ||
export { type BlobResponseType, type EnvironmentContext, type GetDeployStoreOptions, type GetStoreOptions, type GetWithMetadataOptions, type GetWithMetadataResult, type ListOptions, type ListResultBlob, type SetOptions, Store, type StoreOptions, connectLambda, getDeployStore, getStore, listStores, setEnvironmentContext }; |
@@ -6,3 +6,2 @@ import { | ||
REGION_AUTO, | ||
base64Decode, | ||
collectIterator, | ||
@@ -13,5 +12,6 @@ getClientOptions, | ||
setEnvironmentContext | ||
} from "./chunk-XR3MUBBK.js"; | ||
} from "./chunk-IAHPN3L2.js"; | ||
// src/lambda_compat.ts | ||
import { base64Decode } from "@netlify/runtime-utils"; | ||
var connectLambda = (event) => { | ||
@@ -18,0 +18,0 @@ const rawData = base64Decode(event.blobs); |
@@ -1,3 +0,1 @@ | ||
import http from 'node:http'; | ||
type Logger = (...message: unknown[]) => void; | ||
@@ -50,3 +48,2 @@ | ||
private onRequest?; | ||
private port; | ||
private server?; | ||
@@ -57,16 +54,9 @@ private token?; | ||
private dispatchOnRequestEvent; | ||
logDebug(...message: unknown[]): void; | ||
delete(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>; | ||
get(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>; | ||
head(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>; | ||
listBlobs(options: { | ||
dataPath: string; | ||
metadataPath: string; | ||
rootPath: string; | ||
req: http.IncomingMessage; | ||
res: http.ServerResponse; | ||
url: URL; | ||
}): Promise<void>; | ||
listStores(req: http.IncomingMessage, res: http.ServerResponse, rootPath: string, prefix: string): Promise<void>; | ||
put(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>; | ||
private delete; | ||
private get; | ||
private head; | ||
private listBlobs; | ||
private listStores; | ||
private logDebug; | ||
private put; | ||
/** | ||
@@ -76,19 +66,4 @@ * Parses the URL and returns the filesystem paths where entries and metadata | ||
*/ | ||
getLocalPaths(url?: URL): { | ||
rootPath?: undefined; | ||
dataPath?: undefined; | ||
key?: undefined; | ||
metadataPath?: undefined; | ||
} | { | ||
rootPath: string; | ||
dataPath?: undefined; | ||
key?: undefined; | ||
metadataPath?: undefined; | ||
} | { | ||
dataPath: string; | ||
key: string; | ||
metadataPath: string; | ||
rootPath: string; | ||
}; | ||
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void | Promise<void>; | ||
private getLocalPaths; | ||
private handleRequest; | ||
/** | ||
@@ -98,10 +73,9 @@ * Tries to parse a URL as being an API request and returns the different | ||
*/ | ||
parseAPIRequest(req: http.IncomingMessage): { | ||
key: string | undefined; | ||
siteID: string; | ||
storeName: string; | ||
url: URL; | ||
useSignedURL: boolean; | ||
} | null; | ||
sendResponse(req: http.IncomingMessage, res: http.ServerResponse, status: number, body?: string): void; | ||
private parseAPIRequest; | ||
private validateAccess; | ||
/** | ||
* Traverses a path and collects both blobs and directories into a `result` | ||
* object, taking into account the `directories` and `prefix` parameters. | ||
*/ | ||
private static walk; | ||
start(): Promise<{ | ||
@@ -112,11 +86,5 @@ address: string; | ||
}>; | ||
stop(): Promise<void>; | ||
validateAccess(req: http.IncomingMessage): boolean; | ||
/** | ||
* Traverses a path and collects both blobs and directories into a `result` | ||
* object, taking into account the `directories` and `prefix` parameters. | ||
*/ | ||
private static walk; | ||
stop(): Promise<void | undefined>; | ||
} | ||
export { BlobsServer, OnRequestCallback, Operation }; | ||
export { BlobsServer, type OnRequestCallback, Operation }; |
@@ -7,13 +7,11 @@ import { | ||
isNodeError | ||
} from "./chunk-XR3MUBBK.js"; | ||
} from "./chunk-IAHPN3L2.js"; | ||
// src/server.ts | ||
import { createHmac } from "node:crypto"; | ||
import { createReadStream, createWriteStream, promises as fs } from "node:fs"; | ||
import http from "node:http"; | ||
import { createReadStream, promises as fs } from "node:fs"; | ||
import { tmpdir } from "node:os"; | ||
import { dirname, join, relative, resolve, sep } from "node:path"; | ||
import { platform } from "node:process"; | ||
import stream from "node:stream"; | ||
import { promisify } from "node:util"; | ||
import { HTTPServer } from "@netlify/dev-utils"; | ||
var API_URL_PATH = /\/api\/v1\/blobs\/(?<site_id>[^/]+)\/(?<store_name>[^/]+)\/?(?<key>[^?]*)/; | ||
@@ -31,3 +29,2 @@ var LEGACY_API_URL_PATH = /\/api\/v1\/sites\/(?<site_id>[^/]+)\/blobs\/?(?<key>[^?]*)/; | ||
})(Operation || {}); | ||
var pipeline = promisify(stream.pipeline); | ||
var BlobsServer = class _BlobsServer { | ||
@@ -40,23 +37,16 @@ constructor({ debug, directory, logger, onRequest, port, token }) { | ||
this.onRequest = onRequest; | ||
this.port = port || 0; | ||
this.token = token; | ||
this.tokenHash = createHmac("sha256", Math.random.toString()).update(token ?? Math.random.toString()).digest("hex"); | ||
} | ||
dispatchOnRequestEvent(type, url) { | ||
dispatchOnRequestEvent(type, input) { | ||
if (!this.onRequest) { | ||
return; | ||
} | ||
const urlPath = url instanceof URL ? url.pathname + url.search : url; | ||
this.onRequest({ type, url: urlPath }); | ||
const url = new URL(input); | ||
this.onRequest({ type, url: url.pathname + url.search }); | ||
} | ||
logDebug(...message) { | ||
if (!this.debug) { | ||
return; | ||
} | ||
this.logger("[Netlify Blobs server]", ...message); | ||
} | ||
async delete(req, res) { | ||
async delete(req) { | ||
const apiMatch = this.parseAPIRequest(req); | ||
if (apiMatch?.useSignedURL) { | ||
return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); | ||
return Response.json({ url: apiMatch.url.toString() }); | ||
} | ||
@@ -66,3 +56,3 @@ const url = new URL(apiMatch?.url ?? req.url ?? "", this.address); | ||
if (!dataPath || !key) { | ||
return this.sendResponse(req, res, 400); | ||
return new Response(null, { status: 400 }); | ||
} | ||
@@ -77,22 +67,22 @@ try { | ||
if (!isNodeError(error) || error.code !== "ENOENT") { | ||
return this.sendResponse(req, res, 500); | ||
return new Response(null, { status: 500 }); | ||
} | ||
} | ||
return this.sendResponse(req, res, 204); | ||
return new Response(null, { status: 204 }); | ||
} | ||
async get(req, res) { | ||
async get(req) { | ||
const apiMatch = this.parseAPIRequest(req); | ||
const url = apiMatch?.url ?? new URL(req.url ?? "", this.address); | ||
if (apiMatch?.key && apiMatch?.useSignedURL) { | ||
return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); | ||
return Response.json({ url: apiMatch.url.toString() }); | ||
} | ||
const { dataPath, key, metadataPath, rootPath } = this.getLocalPaths(apiMatch?.url ?? url); | ||
if (!rootPath) { | ||
return this.sendResponse(req, res, 400); | ||
return new Response(null, { status: 400 }); | ||
} | ||
if (!dataPath || !metadataPath) { | ||
return this.listStores(req, res, rootPath, url.searchParams.get("prefix") ?? ""); | ||
return this.listStores(rootPath, url.searchParams.get("prefix") ?? ""); | ||
} | ||
if (!key) { | ||
return this.listBlobs({ dataPath, metadataPath, rootPath, req, res, url }); | ||
return this.listBlobs({ dataPath, metadataPath, rootPath, req, url }); | ||
} | ||
@@ -113,19 +103,22 @@ this.dispatchOnRequestEvent("get" /* GET */, url); | ||
} | ||
for (const name in headers) { | ||
res.setHeader(name, headers[name]); | ||
try { | ||
const fileStream = createReadStream(dataPath); | ||
const chunks = []; | ||
for await (const chunk of fileStream) { | ||
chunks.push(Buffer.from(chunk)); | ||
} | ||
const buffer = Buffer.concat(chunks); | ||
return new Response(buffer, { headers }); | ||
} catch (error) { | ||
if (isNodeError(error) && (error.code === "EISDIR" || error.code === "ENOENT")) { | ||
return new Response(null, { status: 404 }); | ||
} | ||
return new Response(null, { status: 500 }); | ||
} | ||
const stream2 = createReadStream(dataPath); | ||
stream2.on("error", (error) => { | ||
if (error.code === "EISDIR" || error.code === "ENOENT") { | ||
return this.sendResponse(req, res, 404); | ||
} | ||
return this.sendResponse(req, res, 500); | ||
}); | ||
stream2.pipe(res); | ||
} | ||
async head(req, res) { | ||
async head(req) { | ||
const url = this.parseAPIRequest(req)?.url ?? new URL(req.url ?? "", this.address); | ||
const { dataPath, key, metadataPath } = this.getLocalPaths(url); | ||
if (!dataPath || !metadataPath || !key) { | ||
return this.sendResponse(req, res, 400); | ||
return new Response(null, { status: 400 }); | ||
} | ||
@@ -136,16 +129,17 @@ try { | ||
const encodedMetadata = encodeMetadata(metadata); | ||
if (encodedMetadata) { | ||
res.setHeader(METADATA_HEADER_INTERNAL, encodedMetadata); | ||
} | ||
return new Response(null, { | ||
headers: { | ||
[METADATA_HEADER_INTERNAL]: encodedMetadata ?? "" | ||
} | ||
}); | ||
} catch (error) { | ||
if (isNodeError(error) && (error.code === "ENOENT" || error.code === "ISDIR")) { | ||
return this.sendResponse(req, res, 404); | ||
return new Response(null, { status: 404 }); | ||
} | ||
this.logDebug("Could not read metadata file:", error); | ||
return this.sendResponse(req, res, 500); | ||
return new Response(null, { status: 500 }); | ||
} | ||
res.end(); | ||
} | ||
async listBlobs(options) { | ||
const { dataPath, rootPath, req, res, url } = options; | ||
const { dataPath, rootPath, req, url } = options; | ||
const directories = url.searchParams.get("directories") === "true"; | ||
@@ -163,22 +157,27 @@ const prefix = url.searchParams.get("prefix") ?? ""; | ||
this.logDebug("Could not perform list:", error); | ||
return this.sendResponse(req, res, 500); | ||
return new Response(null, { status: 500 }); | ||
} | ||
} | ||
res.setHeader("content-type", "application/json"); | ||
return this.sendResponse(req, res, 200, JSON.stringify(result)); | ||
return Response.json(result); | ||
} | ||
async listStores(req, res, rootPath, prefix) { | ||
async listStores(rootPath, prefix) { | ||
try { | ||
const allStores = await fs.readdir(rootPath); | ||
const filteredStores = allStores.map((store) => platform === "win32" ? decodeURIComponent(store) : store).filter((store) => store.startsWith(prefix)); | ||
return this.sendResponse(req, res, 200, JSON.stringify({ stores: filteredStores })); | ||
return Response.json({ stores: filteredStores }); | ||
} catch (error) { | ||
this.logDebug("Could not list stores:", error); | ||
return this.sendResponse(req, res, 500); | ||
return new Response(null, { status: 500 }); | ||
} | ||
} | ||
async put(req, res) { | ||
logDebug(...message) { | ||
if (!this.debug) { | ||
return; | ||
} | ||
this.logger("[Netlify Blobs server]", ...message); | ||
} | ||
async put(req) { | ||
const apiMatch = this.parseAPIRequest(req); | ||
if (apiMatch) { | ||
return this.sendResponse(req, res, 200, JSON.stringify({ url: apiMatch.url.toString() })); | ||
return Response.json({ url: apiMatch.url.toString() }); | ||
} | ||
@@ -188,22 +187,21 @@ const url = new URL(req.url ?? "", this.address); | ||
if (!dataPath || !key || !metadataPath) { | ||
return this.sendResponse(req, res, 400); | ||
return new Response(null, { status: 400 }); | ||
} | ||
const metadataHeader = req.headers[METADATA_HEADER_INTERNAL]; | ||
const metadata = decodeMetadata(Array.isArray(metadataHeader) ? metadataHeader[0] : metadataHeader ?? null); | ||
const metadataHeader = req.headers.get(METADATA_HEADER_INTERNAL); | ||
const metadata = decodeMetadata(metadataHeader); | ||
try { | ||
const tempDirectory = await fs.mkdtemp(join(tmpdir(), "netlify-blobs")); | ||
const relativeDataPath = relative(this.directory, dataPath); | ||
const tempDataPath = join(tempDirectory, relativeDataPath); | ||
await fs.mkdir(dirname(tempDataPath), { recursive: true }); | ||
await pipeline(req, createWriteStream(tempDataPath)); | ||
const tempPath = join(tmpdir(), Math.random().toString()); | ||
const body = await req.arrayBuffer(); | ||
await fs.writeFile(tempPath, Buffer.from(body)); | ||
await fs.mkdir(dirname(dataPath), { recursive: true }); | ||
await fs.copyFile(tempDataPath, dataPath); | ||
await fs.rm(tempDirectory, { force: true, recursive: true }); | ||
await fs.mkdir(dirname(metadataPath), { recursive: true }); | ||
await fs.writeFile(metadataPath, JSON.stringify(metadata)); | ||
await fs.rename(tempPath, dataPath); | ||
if (metadata) { | ||
await fs.mkdir(dirname(metadataPath), { recursive: true }); | ||
await fs.writeFile(metadataPath, JSON.stringify(metadata)); | ||
} | ||
return new Response(null, { status: 200 }); | ||
} catch (error) { | ||
this.logDebug("Error when writing data:", error); | ||
return this.sendResponse(req, res, 500); | ||
return new Response(null, { status: 500 }); | ||
} | ||
return this.sendResponse(req, res, 200); | ||
} | ||
@@ -236,5 +234,5 @@ /** | ||
} | ||
handleRequest(req, res) { | ||
async handleRequest(req) { | ||
if (!req.url || !this.validateAccess(req)) { | ||
return this.sendResponse(req, res, 403); | ||
return new Response(null, { status: 403 }); | ||
} | ||
@@ -244,17 +242,17 @@ switch (req.method?.toLowerCase()) { | ||
this.dispatchOnRequestEvent("delete" /* DELETE */, req.url); | ||
return this.delete(req, res); | ||
return this.delete(req); | ||
} | ||
case "get" /* GET */: { | ||
return this.get(req, res); | ||
return this.get(req); | ||
} | ||
case "put" /* PUT */: { | ||
this.dispatchOnRequestEvent("set" /* SET */, req.url); | ||
return this.put(req, res); | ||
return this.put(req); | ||
} | ||
case "head" /* HEAD */: { | ||
this.dispatchOnRequestEvent("getMetadata" /* GET_METADATA */, req.url); | ||
return this.head(req, res); | ||
return this.head(req); | ||
} | ||
default: | ||
return this.sendResponse(req, res, 405); | ||
return new Response(null, { status: 405 }); | ||
} | ||
@@ -282,3 +280,3 @@ } | ||
url, | ||
useSignedURL: req.headers.accept === SIGNED_URL_ACCEPT_HEADER | ||
useSignedURL: req.headers.get("accept") === SIGNED_URL_ACCEPT_HEADER | ||
}; | ||
@@ -304,35 +302,2 @@ } | ||
} | ||
sendResponse(req, res, status, body) { | ||
this.logDebug(`${req.method} ${req.url} ${status}`); | ||
res.writeHead(status); | ||
res.end(body); | ||
} | ||
async start() { | ||
await fs.mkdir(this.directory, { recursive: true }); | ||
const server = http.createServer((req, res) => this.handleRequest(req, res)); | ||
this.server = server; | ||
return new Promise((resolve2, reject) => { | ||
server.listen(this.port, () => { | ||
const address = server.address(); | ||
if (!address || typeof address === "string") { | ||
return reject(new Error("Server cannot be started on a pipe or Unix socket")); | ||
} | ||
this.address = `http://localhost:${address.port}`; | ||
resolve2(address); | ||
}); | ||
}); | ||
} | ||
async stop() { | ||
if (!this.server) { | ||
return; | ||
} | ||
await new Promise((resolve2, reject) => { | ||
this.server?.close((error) => { | ||
if (error) { | ||
return reject(error); | ||
} | ||
resolve2(null); | ||
}); | ||
}); | ||
} | ||
validateAccess(req) { | ||
@@ -342,3 +307,3 @@ if (!this.token) { | ||
} | ||
const { authorization = "" } = req.headers; | ||
const authorization = req.headers.get("authorization") || ""; | ||
if (authorization.toLowerCase().startsWith("bearer ") && authorization.slice("bearer ".length) === this.token) { | ||
@@ -393,2 +358,18 @@ return true; | ||
} | ||
async start() { | ||
await fs.mkdir(this.directory, { recursive: true }); | ||
const server = new HTTPServer((req) => this.handleRequest(req)); | ||
const address = await server.start(); | ||
const port = Number.parseInt(new URL(address).port); | ||
this.address = address; | ||
this.server = server; | ||
return { | ||
address, | ||
family: "ipv4", | ||
port | ||
}; | ||
} | ||
async stop() { | ||
return this.server?.stop(); | ||
} | ||
}; | ||
@@ -395,0 +376,0 @@ export { |
{ | ||
"name": "@netlify/blobs", | ||
"version": "8.2.0", | ||
"version": "9.0.0", | ||
"description": "A JavaScript client for the Netlify Blob Store", | ||
@@ -69,8 +69,13 @@ "type": "module", | ||
"devDependencies": { | ||
"@types/node": "^22.14.1", | ||
"node-fetch": "^3.3.1", | ||
"semver": "^7.5.3", | ||
"tmp-promise": "^3.0.3", | ||
"tsup": "^7.2.0", | ||
"vitest": "^0.34.0" | ||
"tsup": "^8.0.0", | ||
"vitest": "^3.0.0" | ||
}, | ||
"dependencies": { | ||
"@netlify/dev-utils": "2.0.0", | ||
"@netlify/runtime-utils": "1.1.0" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
1
-50%110361
-4.87%2
Infinity%6
20%2560
-4.26%21
162.5%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added