diff --git a/README.md b/README.md index 6519251..a2168da 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ module.exports.img = createHandler({ // Where the source images are located. // E.g. gs://s.example.com/image.jpg sourceBucket: "s.example.com", + // Where the transformed images needs to be stored. // E.g. gs://c.example.com/image__w_80,h_60.jpg cacheBucket: "c.example.com", diff --git a/lib/main.js b/lib/main.js index d87ec32..6087a6a 100644 --- a/lib/main.js +++ b/lib/main.js @@ -21,10 +21,32 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } + +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } + +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } + +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } + function createHandler(options) { + var _parseBucket = (0, _parse.parseBucket)(options.sourceBucket), + _parseBucket2 = _slicedToArray(_parseBucket, 2), + sourceBucketName = _parseBucket2[0], + sourcePrefix = _parseBucket2[1]; + + var _parseBucket3 = (0, _parse.parseBucket)(options.cacheBucket), + _parseBucket4 = _slicedToArray(_parseBucket3, 2), + cacheBucketName = _parseBucket4[0], + cachePrefix = _parseBucket4[1]; + var storage = new _storage.Storage(options.storage); - var sourceBucket = storage.bucket(options.sourceBucket); - var cacheBucket = storage.bucket(options.cacheBucket); + var sourceBucket = storage.bucket(sourceBucketName); + var cacheBucket = storage.bucket(cacheBucketName); var cacheControl = "public, max-age=31560000, immutable"; var cacheControlInitial = "public, max-age=31560000, s-maxage=0, immutable"; var mergedParams = options.params ? _objectSpread(_objectSpread({}, _params.params), Object.keys(options.params).reduce(function (acc, key) { @@ -40,7 +62,7 @@ function createHandler(options) { target = _parseUrlPath.target, transforms = _parseUrlPath.transforms; - var sourceFile = target ? cacheBucket.file(target) : sourceBucket.file(source); + var sourceFile = target ? cacheBucket.file("".concat(cachePrefix).concat(target)) : sourceBucket.file("".concat(sourcePrefix).concat(source)); sourceFile.createReadStream({ decompress: false }).on("error", _utils.noop).on("response", function (x) { @@ -51,14 +73,14 @@ function createHandler(options) { this.pipe(res); } else if (x.statusCode === 404) { this.end(); - sourceBucket.file(source).createReadStream().on("error", _utils.noop).on("response", function (x) { + sourceBucket.file("".concat(sourcePrefix).concat(source)).createReadStream().on("error", _utils.noop).on("response", function (x) { if (x.statusCode === 200) { res.set("Cache-Control", cacheControlInitial); (0, _transform.transform)(this, transforms).stream(function (err, out) { if (err) { (0, _utils.handleError)(res, err); } else { - var targetFile = cacheBucket.file(target).createWriteStream({ + var targetFile = cacheBucket.file("".concat(cachePrefix).concat(target)).createWriteStream({ contentType: x.headers["content-type"] }).on("error", function (err) { console.error(err); diff --git a/lib/main.js.map b/lib/main.js.map index 00485df..97be8bb 100644 --- a/lib/main.js.map +++ b/lib/main.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/main.ts"],"names":["createHandler","options","storage","Storage","sourceBucket","bucket","cacheBucket","cacheControl","cacheControlInitial","mergedParams","params","Object","keys","reduce","acc","key","handleRequest","req","res","path","decodeURIComponent","source","target","transforms","sourceFile","file","createReadStream","decompress","on","noop","x","statusCode","set","headers","etag","pipe","end","stream","err","out","targetFile","createWriteStream","contentType","console","error","status","send","statusMessage","resume"],"mappings":"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","sourcesContent":["/**\n * Copyright (c) 2020-present Kriasoft | MIT License (https://git.io/JUgVL)\n */\n\nimport { RequestHandler } from \"express\";\nimport { Storage } from \"@google-cloud/storage\";\n\nimport { params } from \"./params\";\nimport { parseUrlPath } from \"./parse\";\nimport { transform } from \"./transform\";\nimport { handleError, noop } from \"./utils\";\nimport type { Options, Params } from \"./types\";\nexport type { Options, Params } from \"./types\";\n\nexport function createHandler(options: Options): RequestHandler {\n const storage = new Storage(options.storage);\n const sourceBucket = storage.bucket(options.sourceBucket);\n const cacheBucket = storage.bucket(options.cacheBucket);\n\n const cacheControl = \"public, max-age=31560000, immutable\";\n const cacheControlInitial = \"public, max-age=31560000, s-maxage=0, immutable\";\n\n const mergedParams = options.params\n ? {\n ...params,\n ...Object.keys(options.params).reduce(\n (acc, key) => ({\n ...acc,\n [key]: {\n ...params[key as keyof Params],\n ...options.params?.[key as keyof Params],\n },\n }),\n {},\n ),\n }\n : params;\n\n return function handleRequest(req, res) {\n const path = decodeURIComponent(req.path);\n const { source, target, transforms } = parseUrlPath(path, mergedParams);\n\n const sourceFile = target\n ? cacheBucket.file(target)\n : sourceBucket.file(source);\n\n sourceFile\n .createReadStream({ decompress: false })\n .on(\"error\", noop)\n .on(\"response\", function (this: NodeJS.ReadStream, x) {\n if (x.statusCode === 200) {\n res.set(\"Content-Type\", x.headers[\"content-type\"]);\n res.set(\"Cache-Control\", cacheControl);\n res.set(\"etag\", x.headers.etag);\n this.pipe(res);\n } else if (x.statusCode === 404) {\n this.end();\n sourceBucket\n .file(source)\n .createReadStream()\n .on(\"error\", noop)\n .on(\"response\", function (this: NodeJS.ReadStream, x) {\n if (x.statusCode === 200) {\n res.set(\"Cache-Control\", cacheControlInitial);\n transform(this, transforms).stream((err, out) => {\n if (err) {\n handleError(res, err);\n } else {\n const targetFile = cacheBucket\n .file(target as string)\n .createWriteStream({\n contentType: x.headers[\"content-type\"],\n })\n .on(\"error\", (err) => {\n console.error(err);\n });\n out.pipe(targetFile);\n out.pipe(res);\n }\n });\n } else {\n this.end(() => {\n res.status(x.statusCode);\n res.send(x.statusMessage);\n });\n }\n })\n .resume();\n } else {\n this.end(() => {\n res.status(x.statusCode);\n res.end(x.statusMessage);\n });\n }\n })\n .resume();\n };\n}\n"],"file":"main.js"} \ No newline at end of file +{"version":3,"sources":["../src/main.ts"],"names":["createHandler","options","sourceBucket","sourceBucketName","sourcePrefix","cacheBucket","cacheBucketName","cachePrefix","storage","Storage","bucket","cacheControl","cacheControlInitial","mergedParams","params","Object","keys","reduce","acc","key","handleRequest","req","res","path","decodeURIComponent","source","target","transforms","sourceFile","file","createReadStream","decompress","on","noop","x","statusCode","set","headers","etag","pipe","end","stream","err","out","targetFile","createWriteStream","contentType","console","error","status","send","statusMessage","resume"],"mappings":"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","sourcesContent":["/**\n * Copyright (c) 2020-present Kriasoft | MIT License (https://git.io/JUgVL)\n */\n\nimport { RequestHandler } from \"express\";\nimport { Storage } from \"@google-cloud/storage\";\n\nimport { params } from \"./params\";\nimport { parseUrlPath, parseBucket } from \"./parse\";\nimport { transform } from \"./transform\";\nimport { handleError, noop } from \"./utils\";\nimport type { Options, Params } from \"./types\";\nexport type { Options, Params } from \"./types\";\n\nexport function createHandler(options: Options): RequestHandler {\n const [sourceBucketName, sourcePrefix] = parseBucket(options.sourceBucket);\n const [cacheBucketName, cachePrefix] = parseBucket(options.cacheBucket);\n\n const storage = new Storage(options.storage);\n const sourceBucket = storage.bucket(sourceBucketName);\n const cacheBucket = storage.bucket(cacheBucketName);\n\n const cacheControl = \"public, max-age=31560000, immutable\";\n const cacheControlInitial = \"public, max-age=31560000, s-maxage=0, immutable\";\n\n const mergedParams = options.params\n ? {\n ...params,\n ...Object.keys(options.params).reduce(\n (acc, key) => ({\n ...acc,\n [key]: {\n ...params[key as keyof Params],\n ...options.params?.[key as keyof Params],\n },\n }),\n {},\n ),\n }\n : params;\n\n return function handleRequest(req, res) {\n const path = decodeURIComponent(req.path);\n const { source, target, transforms } = parseUrlPath(path, mergedParams);\n\n const sourceFile = target\n ? cacheBucket.file(`${cachePrefix}${target}`)\n : sourceBucket.file(`${sourcePrefix}${source}`);\n\n sourceFile\n .createReadStream({ decompress: false })\n .on(\"error\", noop)\n .on(\"response\", function (this: NodeJS.ReadStream, x) {\n if (x.statusCode === 200) {\n res.set(\"Content-Type\", x.headers[\"content-type\"]);\n res.set(\"Cache-Control\", cacheControl);\n res.set(\"etag\", x.headers.etag);\n this.pipe(res);\n } else if (x.statusCode === 404) {\n this.end();\n sourceBucket\n .file(`${sourcePrefix}${source}`)\n .createReadStream()\n .on(\"error\", noop)\n .on(\"response\", function (this: NodeJS.ReadStream, x) {\n if (x.statusCode === 200) {\n res.set(\"Cache-Control\", cacheControlInitial);\n transform(this, transforms).stream((err, out) => {\n if (err) {\n handleError(res, err);\n } else {\n const targetFile = cacheBucket\n .file(`${cachePrefix}${target}`)\n .createWriteStream({\n contentType: x.headers[\"content-type\"],\n })\n .on(\"error\", (err) => {\n console.error(err);\n });\n out.pipe(targetFile);\n out.pipe(res);\n }\n });\n } else {\n this.end(() => {\n res.status(x.statusCode);\n res.send(x.statusMessage);\n });\n }\n })\n .resume();\n } else {\n this.end(() => {\n res.status(x.statusCode);\n res.end(x.statusMessage);\n });\n }\n })\n .resume();\n };\n}\n"],"file":"main.js"} \ No newline at end of file diff --git a/lib/parse.d.ts b/lib/parse.d.ts index 982b536..69aa410 100644 --- a/lib/parse.d.ts +++ b/lib/parse.d.ts @@ -3,3 +3,13 @@ */ import { Params, ParsedOutput } from "./types"; export declare function parseUrlPath(path: string, params: Readonly): ParsedOutput; +/** + * Extracts folder name (prefix) from the bucket name. + * + * @example + * parseBucket("gs://s.example.com") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/uploads") => ["gs://s.example.com", "uploads/"] + * parseBucket("gs://s.example.com/uploads/") => ["gs://s.example.com", "uploads/"] + */ +export declare function parseBucket(value: string): [bucket: string, prefix: string]; diff --git a/lib/parse.js b/lib/parse.js index 2631570..e2da9be 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.parseUrlPath = parseUrlPath; +exports.parseBucket = parseBucket; var _path = require("path"); @@ -101,4 +102,30 @@ function parseUrlPath(path, params) { transforms: transforms }; } +/** + * Extracts folder name (prefix) from the bucket name. + * + * @example + * parseBucket("gs://s.example.com") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/uploads") => ["gs://s.example.com", "uploads/"] + * parseBucket("gs://s.example.com/uploads/") => ["gs://s.example.com", "uploads/"] + */ + + +function parseBucket(value) { + var protocol = function (i) { + return i === -1 ? "gs:" : value.substring(0, i + 1); + }(value.indexOf("://")); + + if (protocol !== "gs:") { + throw new Error("Only Google Storage buckets are supported at the moment."); + } + + return function (i) { + return i === -1 ? [value, ""] : [value.substring(0, i), function (x) { + return x && (x.endsWith("/") ? x : "".concat(x, "/")); + }(value.substring(i + 1))]; + }(value.indexOf("/", protocol ? protocol.length + 2 : 0)); +} //# sourceMappingURL=parse.js.map \ No newline at end of file diff --git a/lib/parse.js.map b/lib/parse.js.map index efff65b..948ac86 100644 --- a/lib/parse.js.map +++ b/lib/parse.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/parse.ts"],"names":["parseUrlPath","path","params","sourceFragments","paramsFragments","transforms","stopParsing","split","forEach","fragment","push","transform","keys","Map","Object","entries","filter","x","undefined","map","name","key","some","val","p","get","param","value","parse","validate","length","source","join","ext","target","substring","startsWith"],"mappings":"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","sourcesContent":["/**\n * Copyright (c) 2020-present Kriasoft | MIT License (https://git.io/JUgVL)\n */\n\nimport { extname } from \"path\";\nimport { Transform, Param, Params, ParsedOutput } from \"./types\";\n\nexport function parseUrlPath(\n path: string,\n params: Readonly,\n): ParsedOutput {\n const sourceFragments: string[] = [];\n const paramsFragments: string[] = [];\n const transforms: Transform[] = [];\n let stopParsing = false;\n\n path.split(\"/\").forEach((fragment) => {\n if (fragment === \"\") {\n sourceFragments.push(fragment);\n return;\n }\n\n if (fragment === \"_\") {\n stopParsing = true;\n return;\n }\n\n if (stopParsing) {\n sourceFragments.push(fragment);\n return;\n }\n\n const transform: Transform = {};\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n const keys = new Map]>(\n Object.entries(params as Required)\n .filter((x) => x !== undefined)\n .map(([name, x]) => [x.key, [name, x]]),\n );\n\n // Attempts to parse out parameters from the URL path fragment.\n // E.g. \"w_80,h_16,c_fill\" => [\"w_80\", \"h_60\", \"c_fill\"]\n fragment.split(\",\").some((x) => {\n const [key, val] = x.split(\"_\");\n const p = val === undefined ? undefined : keys.get(key);\n\n if (p) {\n const [name, param] = p;\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n const value: any = param.parse(val);\n\n if (value !== undefined && param.validate(val) !== false) {\n transform[name as keyof Transform] = value;\n keys.delete(key);\n return false;\n }\n }\n\n return true;\n });\n\n if (Object.keys(transform).length === 0) {\n sourceFragments.push(fragment);\n\n if (!stopParsing && transforms.length > 0) {\n stopParsing = true;\n }\n } else {\n paramsFragments.push(fragment);\n transforms.push(transform);\n }\n });\n\n const source = sourceFragments.join(\"/\");\n const ext = extname(source);\n const target =\n transforms.length > 0\n ? source.substring(0, source.length - ext.length) +\n \"__\" +\n paramsFragments.join(\"__\") +\n ext\n : undefined;\n\n return {\n source: source.startsWith(\"/\") ? source.substring(1) : source,\n target: target?.startsWith(\"/\") ? target.substring(1) : target,\n transforms,\n };\n}\n"],"file":"parse.js"} \ No newline at end of file +{"version":3,"sources":["../src/parse.ts"],"names":["parseUrlPath","path","params","sourceFragments","paramsFragments","transforms","stopParsing","split","forEach","fragment","push","transform","keys","Map","Object","entries","filter","x","undefined","map","name","key","some","val","p","get","param","value","parse","validate","length","source","join","ext","target","substring","startsWith","parseBucket","protocol","i","indexOf","Error","endsWith"],"mappings":"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","sourcesContent":["/**\n * Copyright (c) 2020-present Kriasoft | MIT License (https://git.io/JUgVL)\n */\n\nimport { extname } from \"path\";\nimport { Transform, Param, Params, ParsedOutput } from \"./types\";\n\nexport function parseUrlPath(\n path: string,\n params: Readonly,\n): ParsedOutput {\n const sourceFragments: string[] = [];\n const paramsFragments: string[] = [];\n const transforms: Transform[] = [];\n let stopParsing = false;\n\n path.split(\"/\").forEach((fragment) => {\n if (fragment === \"\") {\n sourceFragments.push(fragment);\n return;\n }\n\n if (fragment === \"_\") {\n stopParsing = true;\n return;\n }\n\n if (stopParsing) {\n sourceFragments.push(fragment);\n return;\n }\n\n const transform: Transform = {};\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n const keys = new Map]>(\n Object.entries(params as Required)\n .filter((x) => x !== undefined)\n .map(([name, x]) => [x.key, [name, x]]),\n );\n\n // Attempts to parse out parameters from the URL path fragment.\n // E.g. \"w_80,h_16,c_fill\" => [\"w_80\", \"h_60\", \"c_fill\"]\n fragment.split(\",\").some((x) => {\n const [key, val] = x.split(\"_\");\n const p = val === undefined ? undefined : keys.get(key);\n\n if (p) {\n const [name, param] = p;\n /* eslint-disable-next-line @typescript-eslint/no-explicit-any */\n const value: any = param.parse(val);\n\n if (value !== undefined && param.validate(val) !== false) {\n transform[name as keyof Transform] = value;\n keys.delete(key);\n return false;\n }\n }\n\n return true;\n });\n\n if (Object.keys(transform).length === 0) {\n sourceFragments.push(fragment);\n\n if (!stopParsing && transforms.length > 0) {\n stopParsing = true;\n }\n } else {\n paramsFragments.push(fragment);\n transforms.push(transform);\n }\n });\n\n const source = sourceFragments.join(\"/\");\n const ext = extname(source);\n const target =\n transforms.length > 0\n ? source.substring(0, source.length - ext.length) +\n \"__\" +\n paramsFragments.join(\"__\") +\n ext\n : undefined;\n\n return {\n source: source.startsWith(\"/\") ? source.substring(1) : source,\n target: target?.startsWith(\"/\") ? target.substring(1) : target,\n transforms,\n };\n}\n\n/**\n * Extracts folder name (prefix) from the bucket name.\n *\n * @example\n * parseBucket(\"gs://s.example.com\") => [\"gs://s.example.com\", \"\"]\n * parseBucket(\"gs://s.example.com/\") => [\"gs://s.example.com\", \"\"]\n * parseBucket(\"gs://s.example.com/uploads\") => [\"gs://s.example.com\", \"uploads/\"]\n * parseBucket(\"gs://s.example.com/uploads/\") => [\"gs://s.example.com\", \"uploads/\"]\n */\nexport function parseBucket(value: string): [bucket: string, prefix: string] {\n const protocol = ((i) => (i === -1 ? \"gs:\" : value.substring(0, i + 1)))(\n value.indexOf(\"://\"),\n );\n\n if (protocol !== \"gs:\") {\n throw new Error(\"Only Google Storage buckets are supported at the moment.\");\n }\n\n return ((i): [bucket: string, prefix: string] => {\n return i === -1\n ? [value, \"\"]\n : [\n value.substring(0, i),\n ((x) => x && (x.endsWith(\"/\") ? x : `${x}/`))(value.substring(i + 1)),\n ];\n })(value.indexOf(\"/\", protocol ? protocol.length + 2 : 0));\n}\n"],"file":"parse.js"} \ No newline at end of file diff --git a/package.json b/package.json index e18c914..bccea9d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "image-resizing", - "version": "0.1.1", + "version": "0.1.2", "description": "Node.js backend (middleware) for image manipulation needs (transform, resize, optimize).", "keywords": [ "cloud functions", diff --git a/src/main.ts b/src/main.ts index 640937a..6f13239 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,16 +6,19 @@ import { RequestHandler } from "express"; import { Storage } from "@google-cloud/storage"; import { params } from "./params"; -import { parseUrlPath } from "./parse"; +import { parseUrlPath, parseBucket } from "./parse"; import { transform } from "./transform"; import { handleError, noop } from "./utils"; import type { Options, Params } from "./types"; export type { Options, Params } from "./types"; export function createHandler(options: Options): RequestHandler { + const [sourceBucketName, sourcePrefix] = parseBucket(options.sourceBucket); + const [cacheBucketName, cachePrefix] = parseBucket(options.cacheBucket); + const storage = new Storage(options.storage); - const sourceBucket = storage.bucket(options.sourceBucket); - const cacheBucket = storage.bucket(options.cacheBucket); + const sourceBucket = storage.bucket(sourceBucketName); + const cacheBucket = storage.bucket(cacheBucketName); const cacheControl = "public, max-age=31560000, immutable"; const cacheControlInitial = "public, max-age=31560000, s-maxage=0, immutable"; @@ -41,8 +44,8 @@ export function createHandler(options: Options): RequestHandler { const { source, target, transforms } = parseUrlPath(path, mergedParams); const sourceFile = target - ? cacheBucket.file(target) - : sourceBucket.file(source); + ? cacheBucket.file(`${cachePrefix}${target}`) + : sourceBucket.file(`${sourcePrefix}${source}`); sourceFile .createReadStream({ decompress: false }) @@ -56,7 +59,7 @@ export function createHandler(options: Options): RequestHandler { } else if (x.statusCode === 404) { this.end(); sourceBucket - .file(source) + .file(`${sourcePrefix}${source}`) .createReadStream() .on("error", noop) .on("response", function (this: NodeJS.ReadStream, x) { @@ -67,7 +70,7 @@ export function createHandler(options: Options): RequestHandler { handleError(res, err); } else { const targetFile = cacheBucket - .file(target as string) + .file(`${cachePrefix}${target}`) .createWriteStream({ contentType: x.headers["content-type"], }) diff --git a/src/parse.test.ts b/src/parse.test.ts index 24066fb..a1f30a7 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -3,7 +3,7 @@ */ import { params } from "./params"; -import { parseUrlPath } from "./parse"; +import { parseUrlPath, parseBucket } from "./parse"; it("/w_60,h_80/example.jpg", () => { const result = parseUrlPath("/w_60,h_80/example.jpg", params); @@ -99,3 +99,59 @@ it("/img/x_10,y_5,w_80,h_60,c_crop/example.jpg", () => { } `); }); + +it('parseBucket("gs://s.example.com")', () => { + const result = parseBucket("gs://s.example.com"); + expect(result).toMatchInlineSnapshot(` + Array [ + "gs://s.example.com", + "", + ] + `); +}); + +it('parseBucket("gs://s.example.com/")', () => { + const result = parseBucket("gs://s.example.com/"); + expect(result).toMatchInlineSnapshot(` + Array [ + "gs://s.example.com", + "", + ] + `); +}); + +it('parseBucket("gs://s.example.com/uploads")', () => { + const result = parseBucket("gs://s.example.com/uploads"); + expect(result).toMatchInlineSnapshot(` + Array [ + "gs://s.example.com", + "uploads/", + ] + `); +}); + +it('parseBucket("gs://s.example.com/uploads/")', () => { + const result = parseBucket("gs://s.example.com/uploads/"); + expect(result).toMatchInlineSnapshot(` + Array [ + "gs://s.example.com", + "uploads/", + ] + `); +}); + +it('parseBucket("s.example.com/uploads/")', () => { + const result = parseBucket("s.example.com/uploads/"); + expect(result).toMatchInlineSnapshot(` + Array [ + "s.example.com", + "uploads/", + ] + `); +}); + +it('parseBucket("err://s.example.com/")', () => { + expect(() => parseBucket("err://s.example.com/")).toThrowError( + "Only Google Storage buckets are supported at the moment.", + ); +}); diff --git a/src/parse.ts b/src/parse.ts index dce7606..71f77d4 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -87,3 +87,31 @@ export function parseUrlPath( transforms, }; } + +/** + * Extracts folder name (prefix) from the bucket name. + * + * @example + * parseBucket("gs://s.example.com") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/") => ["gs://s.example.com", ""] + * parseBucket("gs://s.example.com/uploads") => ["gs://s.example.com", "uploads/"] + * parseBucket("gs://s.example.com/uploads/") => ["gs://s.example.com", "uploads/"] + */ +export function parseBucket(value: string): [bucket: string, prefix: string] { + const protocol = ((i) => (i === -1 ? "gs:" : value.substring(0, i + 1)))( + value.indexOf("://"), + ); + + if (protocol !== "gs:") { + throw new Error("Only Google Storage buckets are supported at the moment."); + } + + return ((i): [bucket: string, prefix: string] => { + return i === -1 + ? [value, ""] + : [ + value.substring(0, i), + ((x) => x && (x.endsWith("/") ? x : `${x}/`))(value.substring(i + 1)), + ]; + })(value.indexOf("/", protocol ? protocol.length + 2 : 0)); +}