Skip to content

fix(storage): set content type on CLI uploads instead of octet-stream#168

Merged
Fermionic-Lyu merged 3 commits into
mainfrom
lyu/storage-upload-content-type
Jun 16, 2026
Merged

fix(storage): set content type on CLI uploads instead of octet-stream#168
Fermionic-Lyu merged 3 commits into
mainfrom
lyu/storage-upload-content-type

Conversation

@Fermionic-Lyu

@Fermionic-Lyu Fermionic-Lyu commented Jun 16, 2026

Copy link
Copy Markdown
Member

Why

Linear INS-358: storage uploads default to application/octet-stream instead of the actual file type.

Verified against source that the CLI is the affected path, on every upload. storage upload built the body with new Blob([fileContent]) and never set a type (src/commands/storage/upload.ts:33). A typeless Blob serializes its multipart part as application/octet-stream, and the backend trusts the client's value — it stores file.mimetype || null and the S3 provider falls back to application/octet-stream; it never infers from the extension. So every CLI-uploaded object got the wrong MIME type.

What

  • Infer the MIME type from the file's extension and set it on the blob, so it reaches the server via the multipart part. This is the right place to infer — the CLI reads a real file off disk (same approach aws s3 cp uses).
  • Add a --content-type <type> flag to override the inferred value.
  • New src/lib/mime.ts helper (small self-contained lookup, no new runtime dependency) + unit tests.
  • Unknown extensions still fall back to application/octet-stream, so there is no regression.
storage upload ./cover.png --bucket images                  # → image/png
storage upload ./data.bin  --bucket files --content-type application/json

Test

  • vitest run src/lib/mime.test.ts ✅ (case-insensitivity, paths, unknown/missing extensions)
  • npm run lint ✅ (0 errors) · npm run build

🤖 Generated with Claude Code


Summary by cubic

CLI storage uploads now send the correct MIME type instead of always application/octet-stream, fixing INS-358. Also bumps @insforge/cli to 0.1.91.

  • Bug Fixes

    • Infer type from the basename’s extension and set it on the multipart Blob; fall back to application/octet-stream for unknown/missing extensions.
    • Enforce precedence: override > inferred > fallback; ignore empty/whitespace --content-type.
  • New Features

    • Adds --content-type <type> to override the inferred type.
    • Introduces src/lib/mime.ts and resolveUploadContentType with unit tests; no new runtime dependencies.

Written for commit 5ca75b3. Summary will update on new commits.

Review in cubic

Note

Set MIME content type on storage upload CLI command based on file extension

  • Adds a --content-type flag to the storage upload command; if omitted, the type is inferred from the file extension via a new mimeTypeFromName utility in mime.ts, falling back to application/octet-stream.
  • mimeTypeFromName covers common extensions across images, documents, text/data, archives, audio, video, and fonts.
  • The resolved type is passed to the Blob constructor so the multipart file part carries the correct Content-Type instead of always sending application/octet-stream.

Changes since #168 opened

  • Modified mimeTypeFromName utility to extract file extensions from the basename only [77bb5bc]
  • Introduced resolveUploadContentType utility and integrated it into storage upload command [77bb5bc]
  • Added test coverage for content type resolution and MIME type inference [77bb5bc]
  • Bumped package version from 0.1.90 to 0.1.91 [5ca75b3]

Macroscope summarized 0742071.

Summary by CodeRabbit

  • New Features

    • Added --content-type flag for storage uploads, allowing explicit MIME type specification or automatic detection from file extension, with application/octet-stream as fallback.
  • Chores

    • Version updated to 0.1.91.

`storage upload` built the multipart blob with `new Blob([fileContent])` and
never set a type, so every uploaded object was stored as
application/octet-stream regardless of the actual file type. The backend trusts
the client's content type and does not infer one from the extension, so the
wrong type stuck.

Infer the MIME type from the file's extension (the approach `aws s3 cp` uses)
and set it on the blob so it reaches the server via the multipart part. Add a
`--content-type` flag to override the inferred value. Unknown extensions still
fall back to application/octet-stream, so there is no regression.

Ref: INS-358

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 906e384d-d4fa-48cd-aca5-e264dd50f374

📥 Commits

Reviewing files that changed from the base of the PR and between 9c44624 and 5ca75b3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • src/commands/storage/upload.test.ts
  • src/commands/storage/upload.ts
  • src/lib/mime.test.ts
  • src/lib/mime.ts

Walkthrough

Adds src/lib/mime.ts with a static extension-to-MIME lookup table and mimeTypeFromName function. Integrates it into the storage upload command via a new exported resolveUploadContentType helper and a --content-type CLI option, so uploaded Blob objects now carry a resolved MIME type. Tests are added for both utilities. Package version bumps to 0.1.91.

Changes

MIME Inference and Upload Integration

Layer / File(s) Summary
MIME lookup table and mimeTypeFromName
src/lib/mime.ts, src/lib/mime.test.ts
Adds a EXTENSION_MIME_TYPES record covering common image, document, text, archive, audio, video, and font extensions. Exports mimeTypeFromName(name), which strips directory components, extracts and lowercases the final extension, and returns the mapped type or undefined. Tests cover extension mapping, case/path handling, basename-only dot behavior, and unknown/missing extension returns.
Upload --content-type option and typed Blob
src/commands/storage/upload.ts, src/commands/storage/upload.test.ts, package.json
Exports resolveUploadContentType(filePath, explicit?), which selects the explicit non-empty --content-type flag value, then falls back to mimeTypeFromName inference, then to application/octet-stream. Registers the new --content-type <type> CLI option on the upload <file> subcommand and passes the resolved type to Blob construction during multipart upload. Tests cover override precedence, extension/path inference, empty/whitespace flag handling, and unknown-extension fallback. Package version bumped to 0.1.91.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • jwfing

Poem

🐇 A bunny hops through file extensions galore,
.png, .pdf, .mp4, and more!
With mimeTypeFromName sniffing each clue,
uploads now whisper their type loud and true.
No more typeless blobs in the night —
every Content-Type is set just right! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lyu/storage-upload-content-type

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/InsForge/CLI/issues/comments/4723773440","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/InsForge/CLI/pull/168?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `84fc64c3-ae0f-457f-a857-be2a9f26d3f5`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between 9c446242404fd8b6c925a6049153686db7240080 and 0742071264c20f6433ec87d3f543c8f4fb399a72.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (3)</summary>\n> \n> * `src/commands/storage/upload.ts`\n> * `src/lib/mime.test.ts`\n> * `src/lib/mime.ts`\n> \n> </details>\n> \n> ```ascii\n>  ________________________________________________________________________________\n> < A bunny is never late, nor is he early, he reviews precisely when he means to. >\n>  --------------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `lyu/storage-upload-content-type`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a longstanding bug where CLI storage uploads always sent application/octet-stream because new Blob([fileContent]) was constructed without a type. The fix infers MIME type from the file extension and adds a --content-type override flag.

  • Introduces src/lib/mime.ts with a static extension-to-MIME lookup and mimeTypeFromName, with correct handling for dotfiles, Windows paths, and trailing dots.
  • Adds resolveUploadContentType (exported for testability) with a clean precedence chain: explicit flag → extension inference → application/octet-stream fallback.
  • Unit tests cover case-insensitivity, full paths, directory dots, unknown/missing extensions, and the explicit-override-wins behavior.

Confidence Score: 5/5

Safe to merge — the change is additive and the unknown-extension fallback to application/octet-stream preserves the exact previous behavior for any file type not in the lookup table.

The fix is narrowly scoped: one Blob constructor call gains a type, one new helper performs a static map lookup, and precedence is well-ordered (explicit flag → inferred → fallback). The fallback guarantees no regression for unrecognized extensions. Tests cover all meaningful branches including whitespace-only overrides, dotfiles, Windows paths, and compound extensions.

No files require special attention.

Important Files Changed

Filename Overview
src/commands/storage/upload.ts Adds --content-type flag and resolveUploadContentType; Blob now carries the resolved type so the multipart part sends the correct Content-Type to the backend.
src/lib/mime.ts New MIME lookup helper; extension stripping correctly handles Unix/Windows paths and edge cases (dotfiles, trailing dots, compound extensions like .tar.gz).
src/lib/mime.test.ts Comprehensive unit tests for mimeTypeFromName covering common types, case-insensitivity, full paths, directory dots, and unknown/missing extensions.
src/commands/storage/upload.test.ts Tests resolveUploadContentType: explicit override, extension inference, whitespace-only flag, and unknown-extension fallback.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CLI as upload.ts
    participant Mime as mime.ts
    participant Server as Backend API

    User->>CLI: storage upload ./photo.png --bucket images
    CLI->>Mime: mimeTypeFromName("./photo.png")
    Mime-->>CLI: "image/png"
    CLI->>CLI: resolveUploadContentType(file, opts.contentType) → "image/png"
    CLI->>CLI: "new Blob([fileContent], { type: "image/png" })"
    CLI->>CLI: formData.append("file", blob, objectKey)
    CLI->>Server: PUT /api/storage/buckets/images/objects/photo.png (multipart: Content-Type: image/png)
    Server-->>CLI: 200 OK
    CLI-->>User: Uploaded "photo.png" to bucket "images".
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CLI as upload.ts
    participant Mime as mime.ts
    participant Server as Backend API

    User->>CLI: storage upload ./photo.png --bucket images
    CLI->>Mime: mimeTypeFromName("./photo.png")
    Mime-->>CLI: "image/png"
    CLI->>CLI: resolveUploadContentType(file, opts.contentType) → "image/png"
    CLI->>CLI: "new Blob([fileContent], { type: "image/png" })"
    CLI->>CLI: formData.append("file", blob, objectKey)
    CLI->>Server: PUT /api/storage/buckets/images/objects/photo.png (multipart: Content-Type: image/png)
    Server-->>CLI: 200 OK
    CLI-->>User: Uploaded "photo.png" to bucket "images".
Loading

Reviews (3): Last reviewed commit: "chore: bump version to 0.1.91" | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Re-trigger cubic

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(storage): set content type on CLI uploads

Summary: A clean, well-scoped bug fix that infers the MIME type from the file extension (with a --content-type override) so CLI uploads stop defaulting to application/octet-stream — the root cause and fix are correctly identified.

Requirements context: No matching spec/plan found under docs/specs/ (the only specs there cover the diagnose and db-migrations commands). Assessed against the PR description and Linear INS-358. The diff matches the stated intent precisely — three files, no scope creep.


Critical

(none)

Suggestion

Software engineering — no test for the command-level resolution logic (src/commands/storage/upload.ts:36-43)
The pure helper mimeTypeFromName is nicely covered, but the precedence chain that actually wires it up — explicit --content-type flag wins → inferred → application/octet-stream fallback, and that the resolved value lands on the Blob type — has no test. This repo does have command-level tests (e.g. src/commands/projects/link.test.ts, src/commands/payments/transactions.test.ts), so a small test asserting that opts.contentType overrides inference and that an unknown extension falls back would fit conventions and lock in the behavior. Non-blocking since the core logic is the well-tested pure helper.

Functionality — inference runs on the full path, not the basename (src/lib/mime.ts:77-81)
mimeTypeFromName calls name.lastIndexOf('.') on the whole string. For a path where a parent directory contains a dot and the file has no extension (e.g. /home/user.name/datafile), ext becomes name/datafile, which isn't a map key, so it returns undefined. This is safe — it can only ever degrade to the octet-stream fallback, never produce a wrong type, because no map key contains / — but it silently skips inference for those paths. Consider extracting the basename first (basename is already imported in upload.ts) so directory dots can't shadow a real extension.

Information

  • --content-type "" (empty string) (src/commands/storage/upload.ts:36-37): ?? only short-circuits on null/undefined, so an explicit empty-string flag value would pass through as ''. A Blob with an empty type effectively serializes as octet-stream, so the outcome is benign, but opts.contentType || mimeTypeFromName(file) || '...' (or trimming) would treat empty as "not provided" if that's the desired semantics. Minor edge case.
  • Naming/imports are consistent with the codebase (.js extension on relative imports, src/lib/*.test.ts colocated tests). The lookup table is a reasonable curated set; unknown extensions correctly fall back, so there's genuinely no regression as claimed.

Dimension coverage

  • Software engineering: Good — pure helper has thorough unit tests (case-insensitivity, multi-dot, missing/trailing-dot, dotfiles). Only gap is the untested command wiring (Suggestion above).
  • Functionality: Solves INS-358 as described. Fallback chain is correct; the path-dot case (Suggestion) only ever degrades safely.
  • Security: No security-relevant regressions. The --content-type user value flows into the Blob type → multipart Content-Type, but the Blob constructor normalizes the type per the WHATWG spec (characters outside U+0020–U+007E force the type to ""), so CRLF/header injection is not possible. No secrets or auth handling touched; the Authorization header is unchanged.
  • Performance: No concerns — a static O(1) lookup table; readFileSync is pre-existing and unchanged.

Verdict: approved (informational)

No blocking issues. The two Suggestions (a command-level test and basename extraction) and the Information notes are worth addressing but do not gate merge. Final GitHub approval remains a human action.

- mimeTypeFromName: strip the directory portion before parsing the extension,
  so a dot in a parent directory (e.g. /home/user.name/datafile) can no longer
  shadow a real extension.
- Extract resolveUploadContentType() and use `||` so an explicit empty/whitespace
  --content-type falls through to inference instead of forcing an empty type.
- Add command-level tests for the precedence chain (override > inferred > fallback)
  and basename-only inference cases.

Review feedback identified by jwfing on PR #168.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jwfing
jwfing previously approved these changes Jun 16, 2026

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - approved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - approved.

@Fermionic-Lyu Fermionic-Lyu merged commit 35d8c13 into main Jun 16, 2026
3 of 4 checks passed
@Fermionic-Lyu Fermionic-Lyu deleted the lyu/storage-upload-content-type branch June 16, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants