No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

files.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. "use strict";
  2. var __asyncValues = (this && this.__asyncValues) || function (o) {
  3. if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
  4. var m = o[Symbol.asyncIterator], i;
  5. return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
  6. function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
  7. function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
  8. };
  9. Object.defineProperty(exports, "__esModule", { value: true });
  10. exports.StorageLayer = exports.StoredFile = void 0;
  11. const fs_1 = require("fs");
  12. const metadata_1 = require("./metadata");
  13. const errors_1 = require("./errors");
  14. const path = require("path");
  15. const fse = require("fs-extra");
  16. const logger_1 = require("../../logger");
  17. const adminSdkConfig_1 = require("../adminSdkConfig");
  18. const types_1 = require("./rules/types");
  19. const upload_1 = require("./upload");
  20. const track_1 = require("../../track");
  21. const types_2 = require("../types");
  22. class StoredFile {
  23. constructor(metadata) {
  24. this.metadata = metadata;
  25. }
  26. get metadata() {
  27. return this._metadata;
  28. }
  29. set metadata(value) {
  30. this._metadata = value;
  31. }
  32. }
  33. exports.StoredFile = StoredFile;
  34. const TRAILING_SLASHES_PATTERN = /\/+$/;
  35. class StorageLayer {
  36. constructor(_projectId, _files, _buckets, _rulesValidator, _adminCredsValidator, _persistence, _cloudFunctions) {
  37. this._projectId = _projectId;
  38. this._files = _files;
  39. this._buckets = _buckets;
  40. this._rulesValidator = _rulesValidator;
  41. this._adminCredsValidator = _adminCredsValidator;
  42. this._persistence = _persistence;
  43. this._cloudFunctions = _cloudFunctions;
  44. }
  45. createBucket(id) {
  46. if (!this._buckets.has(id)) {
  47. this._buckets.set(id, new metadata_1.CloudStorageBucketMetadata(id));
  48. }
  49. }
  50. async listBuckets() {
  51. if (this._buckets.size === 0) {
  52. let adminSdkConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(this._projectId);
  53. if (!adminSdkConfig) {
  54. adminSdkConfig = (0, adminSdkConfig_1.constructDefaultAdminSdkConfig)(this._projectId);
  55. }
  56. this.createBucket(adminSdkConfig.storageBucket);
  57. }
  58. return [...this._buckets.values()];
  59. }
  60. async getObject(request) {
  61. var _a;
  62. const metadata = this.getMetadata(request.bucketId, request.decodedObjectId);
  63. const hasValidDownloadToken = ((metadata === null || metadata === void 0 ? void 0 : metadata.downloadTokens) || []).includes((_a = request.downloadToken) !== null && _a !== void 0 ? _a : "");
  64. let authorized = hasValidDownloadToken;
  65. if (!authorized) {
  66. authorized = await this._rulesValidator.validate(["b", request.bucketId, "o", request.decodedObjectId].join("/"), request.bucketId, types_1.RulesetOperationMethod.GET, { before: metadata === null || metadata === void 0 ? void 0 : metadata.asRulesResource() }, this._projectId, request.authorization);
  67. }
  68. if (!authorized) {
  69. throw new errors_1.ForbiddenError("Failed auth");
  70. }
  71. if (!metadata) {
  72. throw new errors_1.NotFoundError("File not found");
  73. }
  74. return { metadata: metadata, data: this.getBytes(request.bucketId, request.decodedObjectId) };
  75. }
  76. getMetadata(bucket, object) {
  77. const key = this.path(bucket, object);
  78. const val = this._files.get(key);
  79. if (val) {
  80. return val.metadata;
  81. }
  82. return;
  83. }
  84. getBytes(bucket, object, size, offset) {
  85. const key = this.path(bucket, object);
  86. const val = this._files.get(key);
  87. if (val) {
  88. const len = size ? size : Number(val.metadata.size);
  89. return this._persistence.readBytes(this.path(bucket, object), len, offset);
  90. }
  91. return undefined;
  92. }
  93. async deleteObject(request) {
  94. const storedMetadata = this.getMetadata(request.bucketId, request.decodedObjectId);
  95. const authorized = await this._rulesValidator.validate(["b", request.bucketId, "o", request.decodedObjectId].join("/"), request.bucketId, types_1.RulesetOperationMethod.DELETE, { before: storedMetadata === null || storedMetadata === void 0 ? void 0 : storedMetadata.asRulesResource() }, this._projectId, request.authorization);
  96. if (!authorized) {
  97. throw new errors_1.ForbiddenError();
  98. }
  99. if (!storedMetadata) {
  100. throw new errors_1.NotFoundError();
  101. }
  102. this.deleteFile(request.bucketId, request.decodedObjectId);
  103. }
  104. deleteFile(bucketId, objectId) {
  105. const isFolder = objectId.toLowerCase().endsWith("%2f");
  106. if (isFolder) {
  107. objectId = objectId.slice(0, -3);
  108. }
  109. let filePath = this.path(bucketId, objectId);
  110. if (isFolder) {
  111. filePath += "%2F";
  112. }
  113. const file = this._files.get(filePath);
  114. if (file === undefined) {
  115. return false;
  116. }
  117. else {
  118. this._files.delete(filePath);
  119. this._persistence.deleteFile(filePath);
  120. this._cloudFunctions.dispatch("delete", new metadata_1.CloudStorageObjectMetadata(file.metadata));
  121. return true;
  122. }
  123. }
  124. async updateObjectMetadata(request) {
  125. const storedMetadata = this.getMetadata(request.bucketId, request.decodedObjectId);
  126. const authorized = await this._rulesValidator.validate(["b", request.bucketId, "o", request.decodedObjectId].join("/"), request.bucketId, types_1.RulesetOperationMethod.UPDATE, {
  127. before: storedMetadata === null || storedMetadata === void 0 ? void 0 : storedMetadata.asRulesResource(),
  128. after: storedMetadata === null || storedMetadata === void 0 ? void 0 : storedMetadata.asRulesResource(request.metadata),
  129. }, this._projectId, request.authorization);
  130. if (!authorized) {
  131. throw new errors_1.ForbiddenError();
  132. }
  133. if (!storedMetadata) {
  134. throw new errors_1.NotFoundError();
  135. }
  136. storedMetadata.update(request.metadata);
  137. return storedMetadata;
  138. }
  139. async uploadObject(upload) {
  140. if (upload.status !== upload_1.UploadStatus.FINISHED) {
  141. throw new Error(`Unexpected upload status encountered: ${upload.status}.`);
  142. }
  143. const storedMetadata = this.getMetadata(upload.bucketId, upload.objectId);
  144. const filePath = this.path(upload.bucketId, upload.objectId);
  145. function getIncomingMetadata(field) {
  146. if (!upload.metadata) {
  147. return undefined;
  148. }
  149. const value = upload.metadata[field];
  150. return value === null ? undefined : value;
  151. }
  152. const metadata = new metadata_1.StoredFileMetadata({
  153. name: upload.objectId,
  154. bucket: upload.bucketId,
  155. contentType: getIncomingMetadata("contentType"),
  156. contentDisposition: getIncomingMetadata("contentDisposition"),
  157. contentEncoding: getIncomingMetadata("contentEncoding"),
  158. contentLanguage: getIncomingMetadata("contentLanguage"),
  159. cacheControl: getIncomingMetadata("cacheControl"),
  160. customMetadata: getIncomingMetadata("metadata"),
  161. }, this._cloudFunctions, this._persistence.readBytes(upload.path, upload.size));
  162. const authorized = await this._rulesValidator.validate(["b", upload.bucketId, "o", upload.objectId].join("/"), upload.bucketId, types_1.RulesetOperationMethod.CREATE, {
  163. before: storedMetadata === null || storedMetadata === void 0 ? void 0 : storedMetadata.asRulesResource(),
  164. after: metadata.asRulesResource(),
  165. }, this._projectId, upload.authorization);
  166. if (!authorized) {
  167. this._persistence.deleteFile(upload.path);
  168. throw new errors_1.ForbiddenError();
  169. }
  170. this._persistence.deleteFile(filePath, true);
  171. this._persistence.renameFile(upload.path, filePath);
  172. this._files.set(filePath, new StoredFile(metadata));
  173. this._cloudFunctions.dispatch("finalize", new metadata_1.CloudStorageObjectMetadata(metadata));
  174. return metadata;
  175. }
  176. copyObject({ sourceBucket, sourceObject, destinationBucket, destinationObject, incomingMetadata, authorization, }) {
  177. if (!this._adminCredsValidator.validate(authorization)) {
  178. throw new errors_1.ForbiddenError();
  179. }
  180. const sourceMetadata = this.getMetadata(sourceBucket, sourceObject);
  181. if (!sourceMetadata) {
  182. throw new errors_1.NotFoundError();
  183. }
  184. const sourceBytes = this.getBytes(sourceBucket, sourceObject);
  185. const destinationFilePath = this.path(destinationBucket, destinationObject);
  186. this._persistence.deleteFile(destinationFilePath, true);
  187. this._persistence.appendBytes(destinationFilePath, sourceBytes);
  188. const newMetadata = Object.assign(Object.assign(Object.assign({}, sourceMetadata), { metadata: sourceMetadata.customMetadata }), incomingMetadata);
  189. if (sourceMetadata.downloadTokens.length &&
  190. !((incomingMetadata === null || incomingMetadata === void 0 ? void 0 : incomingMetadata.metadata) && Object.keys(incomingMetadata === null || incomingMetadata === void 0 ? void 0 : incomingMetadata.metadata).length)) {
  191. if (!newMetadata.metadata)
  192. newMetadata.metadata = {};
  193. newMetadata.metadata.firebaseStorageDownloadTokens = sourceMetadata.downloadTokens.join(",");
  194. }
  195. if (newMetadata.metadata) {
  196. for (const [k, v] of Object.entries(newMetadata.metadata)) {
  197. if (v === null)
  198. newMetadata.metadata[k] = "";
  199. }
  200. }
  201. function getMetadata(field) {
  202. const value = newMetadata[field];
  203. return value === null ? undefined : value;
  204. }
  205. const copiedFileMetadata = new metadata_1.StoredFileMetadata({
  206. name: destinationObject,
  207. bucket: destinationBucket,
  208. contentType: getMetadata("contentType"),
  209. contentDisposition: getMetadata("contentDisposition"),
  210. contentEncoding: getMetadata("contentEncoding"),
  211. contentLanguage: getMetadata("contentLanguage"),
  212. cacheControl: getMetadata("cacheControl"),
  213. customMetadata: getMetadata("metadata"),
  214. }, this._cloudFunctions, sourceBytes);
  215. const file = new StoredFile(copiedFileMetadata);
  216. this._files.set(destinationFilePath, file);
  217. this._cloudFunctions.dispatch("finalize", new metadata_1.CloudStorageObjectMetadata(file.metadata));
  218. return file.metadata;
  219. }
  220. async listObjects(request) {
  221. var _a;
  222. const { bucketId, prefix, delimiter, pageToken, authorization } = request;
  223. const authorized = await this._rulesValidator.validate(["b", bucketId, "o", prefix.replace(TRAILING_SLASHES_PATTERN, "")].join("/"), bucketId, types_1.RulesetOperationMethod.LIST, {}, this._projectId, authorization, delimiter);
  224. if (!authorized) {
  225. throw new errors_1.ForbiddenError();
  226. }
  227. let items = [];
  228. const prefixes = new Set();
  229. for (const [, file] of this._files) {
  230. if (file.metadata.bucket !== bucketId) {
  231. continue;
  232. }
  233. const name = file.metadata.name;
  234. if (!name.startsWith(prefix)) {
  235. continue;
  236. }
  237. let includeMetadata = true;
  238. if (delimiter) {
  239. const delimiterIdx = name.indexOf(delimiter);
  240. const delimiterAfterPrefixIdx = name.indexOf(delimiter, prefix.length);
  241. includeMetadata = delimiterIdx === -1 || delimiterAfterPrefixIdx === -1;
  242. if (delimiterAfterPrefixIdx !== -1) {
  243. prefixes.add(name.slice(0, delimiterAfterPrefixIdx + delimiter.length));
  244. }
  245. }
  246. if (includeMetadata) {
  247. items.push(file.metadata);
  248. }
  249. }
  250. items.sort((a, b) => {
  251. if (a.name === b.name) {
  252. return 0;
  253. }
  254. else if (a.name < b.name) {
  255. return -1;
  256. }
  257. else {
  258. return 1;
  259. }
  260. });
  261. if (pageToken) {
  262. const idx = items.findIndex((v) => v.name === pageToken);
  263. if (idx !== -1) {
  264. items = items.slice(idx);
  265. }
  266. }
  267. const maxResults = (_a = request.maxResults) !== null && _a !== void 0 ? _a : 1000;
  268. let nextPageToken = undefined;
  269. if (items.length > maxResults) {
  270. nextPageToken = items[maxResults].name;
  271. items = items.slice(0, maxResults);
  272. }
  273. return {
  274. nextPageToken,
  275. prefixes: prefixes.size > 0 ? [...prefixes].sort() : undefined,
  276. items: items.length > 0 ? items : undefined,
  277. };
  278. }
  279. createDownloadToken(request) {
  280. if (!this._adminCredsValidator.validate(request.authorization)) {
  281. throw new errors_1.ForbiddenError();
  282. }
  283. const metadata = this.getMetadata(request.bucketId, request.decodedObjectId);
  284. if (!metadata) {
  285. throw new errors_1.NotFoundError();
  286. }
  287. metadata.addDownloadToken();
  288. return metadata;
  289. }
  290. deleteDownloadToken(request) {
  291. if (!this._adminCredsValidator.validate(request.authorization)) {
  292. throw new errors_1.ForbiddenError();
  293. }
  294. const metadata = this.getMetadata(request.bucketId, request.decodedObjectId);
  295. if (!metadata) {
  296. throw new errors_1.NotFoundError();
  297. }
  298. metadata.deleteDownloadToken(request.token);
  299. return metadata;
  300. }
  301. path(bucket, object) {
  302. return path.join(bucket, object);
  303. }
  304. get dirPath() {
  305. return this._persistence.dirPath;
  306. }
  307. async export(storageExportPath, options) {
  308. var e_1, _a;
  309. const bucketsList = {
  310. buckets: [],
  311. };
  312. for (const b of await this.listBuckets()) {
  313. bucketsList.buckets.push({ id: b.id });
  314. }
  315. void (0, track_1.trackEmulator)("emulator_export", {
  316. initiated_by: options.initiatedBy,
  317. emulator_name: types_2.Emulators.STORAGE,
  318. count: bucketsList.buckets.length,
  319. });
  320. const bucketsFilePath = path.join(storageExportPath, "buckets.json");
  321. await fse.writeFile(bucketsFilePath, JSON.stringify(bucketsList, undefined, 2));
  322. const blobsDirPath = path.join(storageExportPath, "blobs");
  323. await fse.ensureDir(blobsDirPath);
  324. const metadataDirPath = path.join(storageExportPath, "metadata");
  325. await fse.ensureDir(metadataDirPath);
  326. try {
  327. for (var _b = __asyncValues(this._files.entries()), _c; _c = await _b.next(), !_c.done;) {
  328. const [, file] = _c.value;
  329. const diskFileName = this._persistence.getDiskFileName(this.path(file.metadata.bucket, file.metadata.name));
  330. await fse.copy(path.join(this.dirPath, diskFileName), path.join(blobsDirPath, diskFileName));
  331. const metadataExportPath = path.join(metadataDirPath, encodeURIComponent(diskFileName)) + ".json";
  332. await fse.writeFile(metadataExportPath, metadata_1.StoredFileMetadata.toJSON(file.metadata));
  333. }
  334. }
  335. catch (e_1_1) { e_1 = { error: e_1_1 }; }
  336. finally {
  337. try {
  338. if (_c && !_c.done && (_a = _b.return)) await _a.call(_b);
  339. }
  340. finally { if (e_1) throw e_1.error; }
  341. }
  342. }
  343. import(storageExportPath, options) {
  344. const bucketsFile = path.join(storageExportPath, "buckets.json");
  345. const bucketsList = JSON.parse((0, fs_1.readFileSync)(bucketsFile, "utf-8"));
  346. void (0, track_1.trackEmulator)("emulator_import", {
  347. initiated_by: options.initiatedBy,
  348. emulator_name: types_2.Emulators.STORAGE,
  349. count: bucketsList.buckets.length,
  350. });
  351. for (const b of bucketsList.buckets) {
  352. const bucketMetadata = new metadata_1.CloudStorageBucketMetadata(b.id);
  353. this._buckets.set(b.id, bucketMetadata);
  354. }
  355. const metadataDir = path.join(storageExportPath, "metadata");
  356. const blobsDir = path.join(storageExportPath, "blobs");
  357. if (!(0, fs_1.existsSync)(metadataDir) || !(0, fs_1.existsSync)(blobsDir)) {
  358. logger_1.logger.warn(`Could not find metadata directory at "${metadataDir}" and/or blobs directory at "${blobsDir}".`);
  359. return;
  360. }
  361. const metadataList = this.walkDirSync(metadataDir);
  362. const dotJson = ".json";
  363. for (const f of metadataList) {
  364. if (path.extname(f) !== dotJson) {
  365. logger_1.logger.debug(`Skipping unexpected storage metadata file: ${f}`);
  366. continue;
  367. }
  368. const metadata = metadata_1.StoredFileMetadata.fromJSON((0, fs_1.readFileSync)(f, "utf-8"), this._cloudFunctions);
  369. const metadataRelPath = path.relative(metadataDir, f);
  370. const blobPath = metadataRelPath.substring(0, metadataRelPath.length - dotJson.length);
  371. const blobAbsPath = path.join(blobsDir, blobPath);
  372. if (!(0, fs_1.existsSync)(blobAbsPath)) {
  373. logger_1.logger.warn(`Could not find file "${blobPath}" in storage export.`);
  374. continue;
  375. }
  376. let fileName = metadata.name;
  377. const objectNameSep = getPathSep(fileName);
  378. if (fileName !== path.sep) {
  379. fileName = fileName.split(objectNameSep).join(path.sep);
  380. }
  381. const filepath = this.path(metadata.bucket, fileName);
  382. this._persistence.copyFromExternalPath(blobAbsPath, filepath);
  383. this._files.set(filepath, new StoredFile(metadata));
  384. }
  385. }
  386. *walkDirSync(dir) {
  387. const files = (0, fs_1.readdirSync)(dir);
  388. for (const file of files) {
  389. const p = path.join(dir, file);
  390. if ((0, fs_1.statSync)(p).isDirectory()) {
  391. yield* this.walkDirSync(p);
  392. }
  393. else {
  394. yield p;
  395. }
  396. }
  397. }
  398. }
  399. exports.StorageLayer = StorageLayer;
  400. function getPathSep(decodedPath) {
  401. const firstSepIndex = decodedPath.search(/[\/|\\\\]/g);
  402. return decodedPath[firstSepIndex];
  403. }