Skip to main content

Temporary File Upload

Overview

A shared upload service used by both forms (file_upload questions) and the bulletin board (images in posts and comments). Files land under the temp/ prefix first, and the owning feature moves them to permanent storage when the real record is saved.

This feature never touches the database, and both endpoints are public — no LIFF token required. They should therefore be read together with the consumer-side commit mechanism, which is what actually proves ownership of a file.

Business Flow

POST /api/upload-file/temp (multipart, field named file)

  1. A missing or unreadable file field returns 400 file must be a file (the equivalent of @IsFile()).
  2. Anything over 10 MB returns 400 file must be smaller than or equal to 10485760 bytes (the equivalent of @MaxFileSize).
  3. The buffer is read and the mime type is detected from the actual file content via util.DetectExtension and util.MIMEForExt; the client-supplied Content-Type is not trusted. This mirrors the behavior of nestjs-form-data.
  4. Allowed mime types are image/jpeg, image/jpg, image/png, and application/pdf. Anything else returns 400 file has invalid mime type.
  5. PutTemp writes the object as temp/temp-{unixMilli}.{ext} and returns {path: publicURL}.

POST /api/upload-file/temp-base64 (JSON with a file field)

  1. DTO validation requires file to be a non-empty string (@IsString and @IsNotEmpty).
  2. DetectExtensionFromBase64 identifies the type from the file signature. If it cannot, the response is 400 Unable to detect file type. Supported types: jpg, png, gif, pdf, webp — note that this set is broader than the multipart endpoint's.
  3. createFileFromBase64 strips the data-URI prefix (everything before the first ,) and base64-decodes, trying standard encoding first and then raw. A failed decode leaves an empty buffer and falls through to the next check (in the source, JS Buffer.from never throws, so the catch was dead code). An empty buffer returns 400 Empty file content; an unmappable mime becomes application/octet-stream; and originalName is set to file-{unixMilli}.
  4. PutTemp writes the object as temp/temp-{unixMilli}.{ext} and captures the public URL.
  5. CopyObject then copies to public/mock-line-oa-hash/images/{filename}. This was ported straight from the source even though nothing consumes that destination — deliberate parity.
  6. The response is {path: publicURL}, pointing at the file in temp/, not at the copy.

What happens next

The follow-up steps matter more than the endpoints themselves.

  • Forms: the returned value is a URL under temp/. At submit time, Form Answer Submission copies it to form-builder/{formId}/{filename}, validating the filename against ^[a-zA-Z0-9._-]+$.
  • Bulletin board: post writing goes through an image committer that calls HeadObject to prove the object really exists before copying. This is the only barrier against referencing another tenant's object or a key that was never uploaded, and it accepts both a public URL and a raw key matching ^temp/temp-[0-9]+\.[A-Za-z0-9]+$.
  • Objects under temp/ are never deleted by any request; sweeping that prefix is a separate operations task.

Key Files & Functions

RouteHandler
POST /api/upload-file/tempinternal/uploadfile/handler.go(*Handler).UploadFileTemp
POST /api/upload-file/temp-base64(*Handler).UploadFileTempBase64
  • internal/uploadfile/register.goRegister(r, deps)
  • internal/uploadfile/service.goNewService, UploadFileTemp, UploadFileTempBase64, createFileFromBase64, nowMilli, plus the storage interface
  • internal/uploadfile/handler.gotempBase64DTO, allowedTempMimes, and maxTempFileSize set to 10*1024*1024
  • internal/storagex/storagex.goNew(cfg), PutTemp, GetPublicURL, CopyObject, HeadObject, IsFileExist, and the UploadFile type
  • internal/s3x/s3x.go — the client built on aws-sdk-go-v2
  • internal/util/hash.go and base64.goDetectExtension, DetectExtensionFromBase64, MIMEForExt

Connections to Other Services

  • Object storage (S3/MinIO) only — no database, no Redis.
  • The deps.Config.Storage configuration: endpoint, bucket, and PublicHost, the last of which is used to strip the prefix during the form-side copy.
  • Downstream consumers: Form Answer Submission, plus bulletin post and comment writing.
  • The related client-web feature is file-upload.