export function createCloudEndpoints()

in src/emulator/storage/apis/gcloud.ts [19:282]


export function createCloudEndpoints(emulator: StorageEmulator): Router {
  // eslint-disable-next-line new-cap
  const gcloudStorageAPI = Router();
  const { storageLayer } = emulator;

  // Automatically create a bucket for any route which uses a bucket
  gcloudStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => {
    storageLayer.createBucket(req.params[0]);
    next();
  });

  gcloudStorageAPI.get("/b", async (req, res) => {
    res.json({
      kind: "storage#buckets",
      items: await storageLayer.listBuckets(),
    });
  });

  gcloudStorageAPI.get(
    ["/b/:bucketId/o/:objectId", "/download/storage/v1/b/:bucketId/o/:objectId"],
    (req, res) => {
      const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

      if (!md) {
        res.sendStatus(404);
        return;
      }

      if (req.query.alt == "media") {
        return sendFileBytes(md, storageLayer, req, res);
      }

      const outgoingMd = new CloudStorageObjectMetadata(md);

      res.json(outgoingMd).status(200).send();
      return;
    }
  );

  gcloudStorageAPI.patch("/b/:bucketId/o/:objectId", (req, res) => {
    const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    md.update(req.body);

    const outgoingMetadata = new CloudStorageObjectMetadata(md);
    res.json(outgoingMetadata).status(200).send();
    return;
  });

  gcloudStorageAPI.get("/b/:bucketId/o", (req, res) => {
    // TODO validate that all query params are single strings and are not repeated.
    let maxRes = undefined;
    if (req.query.maxResults) {
      maxRes = +req.query.maxResults.toString();
    }
    const delimiter = req.query.delimiter ? req.query.delimiter.toString() : "/";
    const pageToken = req.query.pageToken ? req.query.pageToken.toString() : undefined;
    const prefix = req.query.prefix ? req.query.prefix.toString() : "";

    const listResult = storageLayer.listItems(
      req.params.bucketId,
      prefix,
      delimiter,
      pageToken,
      maxRes
    );

    res.json(listResult);
  });

  gcloudStorageAPI.delete("/b/:bucketId/o/:objectId", (req, res) => {
    const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    storageLayer.deleteFile(req.params.bucketId, req.params.objectId);
    res.status(200).send();
  });

  gcloudStorageAPI.put("/upload/storage/v1/b/:bucketId/o", async (req, res) => {
    if (!req.query.upload_id) {
      res.sendStatus(400);
      return;
    }

    const uploadId = req.query.upload_id.toString();

    const bufs: Buffer[] = [];
    req.on("data", (data) => {
      bufs.push(data);
    });

    await new Promise<void>((resolve) => {
      req.on("end", () => {
        req.body = Buffer.concat(bufs);
        resolve();
      });
    });

    let upload = storageLayer.uploadBytes(uploadId, req.body);

    if (!upload) {
      res.sendStatus(400);
      return;
    }

    const finalizedUpload = storageLayer.finalizeUpload(uploadId);
    if (!finalizedUpload) {
      res.sendStatus(400);
      return;
    }
    upload = finalizedUpload.upload;
    res.status(200).json(new CloudStorageObjectMetadata(finalizedUpload.file.metadata)).send();
  });

  gcloudStorageAPI.post("/b/:bucketId/o/:objectId/acl", (req, res) => {
    // TODO(abehaskins) Link to a doc with more info
    EmulatorLogger.forEmulator(Emulators.STORAGE).log(
      "WARN_ONCE",
      "Cloud Storage ACLs are not supported in the Storage Emulator. All related methods will succeed, but have no effect."
    );
    const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    // We do an empty update to step metageneration forward;
    md.update({});

    res
      .json({
        kind: "storage#objectAccessControl",
        object: md.name,
        id: `${req.params.bucketId}/${md.name}/${md.generation}/allUsers`,
        selfLink: `http://${EmulatorRegistry.getInfo(Emulators.STORAGE)?.host}:${
          EmulatorRegistry.getInfo(Emulators.STORAGE)?.port
        }/storage/v1/b/${md.bucket}/o/${encodeURIComponent(md.name)}/acl/allUsers`,
        bucket: md.bucket,
        entity: req.body.entity,
        role: req.body.role,
        etag: "someEtag",
        generation: md.generation.toString(),
      } as CloudStorageObjectAccessControlMetadata)
      .status(200);
  });

  gcloudStorageAPI.post("/upload/storage/v1/b/:bucketId/o", (req, res) => {
    if (!req.query.name) {
      res.sendStatus(400);
      return;
    }
    let name = req.query.name.toString();

    if (name.startsWith("/")) {
      name = name.slice(1);
    }

    const contentType = req.header("content-type") || req.header("x-upload-content-type");

    if (!contentType) {
      res.sendStatus(400);
      return;
    }

    if (req.query.uploadType == "resumable") {
      const upload = storageLayer.startUpload(req.params.bucketId, name, contentType, req.body);
      const emulatorInfo = EmulatorRegistry.getInfo(Emulators.STORAGE);

      if (emulatorInfo == undefined) {
        res.sendStatus(500);
        return;
      }

      const { host, port } = emulatorInfo;
      const uploadUrl = `http://${host}:${port}/upload/storage/v1/b/${upload.bucketId}/o?name=${upload.fileLocation}&uploadType=resumable&upload_id=${upload.uploadId}`;
      res.header("location", uploadUrl).status(200).send();
      return;
    }

    if (!contentType.startsWith("multipart/related")) {
      res.sendStatus(400);
      return;
    }

    const boundary = `--${contentType.split("boundary=")[1]}`;
    const bodyString = req.body.toString();

    const bodyStringParts = bodyString.split(boundary).filter((v: string) => v);

    const metadataString = bodyStringParts[0].split(/\r?\n/)[3];
    const blobParts = bodyStringParts[1].split(/\r?\n/);
    const blobContentTypeString = blobParts[1];

    if (!blobContentTypeString || !blobContentTypeString.startsWith("Content-Type: ")) {
      res.sendStatus(400);
      return;
    }

    const blobContentType = blobContentTypeString.slice("Content-Type: ".length);
    const bodyBuffer = req.body as Buffer;

    const metadataSegment = `${boundary}${bodyString.split(boundary)[1]}`;
    const dataSegment = `${boundary}${bodyString.split(boundary).slice(2)[0]}`;
    const dataSegmentHeader = (dataSegment.match(/.+Content-Type:.+?\r?\n\r?\n/s) || [])[0];

    if (!dataSegmentHeader) {
      res.sendStatus(400);
      return;
    }

    const bufferOffset = metadataSegment.length + dataSegmentHeader.length;

    const blobBytes = Buffer.from(bodyBuffer.slice(bufferOffset, -`\r\n${boundary}--`.length));

    const metadata = storageLayer.oneShotUpload(
      req.params.bucketId,
      name,
      blobContentType,
      JSON.parse(metadataString),
      blobBytes
    );

    if (!metadata) {
      res.sendStatus(400);
      return;
    }

    res.status(200).json(new CloudStorageObjectMetadata(metadata)).send();
    return;
  });

  gcloudStorageAPI.get("/:bucketId/:objectId(**)", (req, res) => {
    const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    return sendFileBytes(md, storageLayer, req, res);
  });

  gcloudStorageAPI.all("/**", (req, res) => {
    if (process.env.STORAGE_EMULATOR_DEBUG) {
      console.table(req.headers);
      console.log(req.method, req.url);
      res.json("endpoint not implemented");
    } else {
      res.sendStatus(501);
    }
  });

  return gcloudStorageAPI;
}