Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions cmd/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,9 +446,110 @@ func mediaBlockTypeForContentType(contentType string) string {
}
}

var fileGetCmd = &cobra.Command{
Use: "get <upload-id>",
Short: "Retrieve a file upload by ID",
Long: `Retrieve a single file upload by its ID, e.g. to check 'status'
(pending / uploaded / expired) or recover an existing file_upload id for
re-use in a block.

Examples:
notion file get 351d45fb-804f-8151-a5ea-00b27d0b9258
notion file get 351d45fb-... --format json`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
token, err := getToken()
if err != nil {
return err
}

uploadID := strings.TrimSpace(args[0])
if uploadID == "" {
return fmt.Errorf("upload id is required")
}

c := client.New(token)
c.SetDebug(debugMode)

data, err := c.Get("/v1/file_uploads/" + uploadID)
if err != nil {
return fmt.Errorf("get file upload: %w", err)
}

var result map[string]interface{}
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("parse response: %w", err)
}

if outputFormat == "json" {
return render.JSON(result)
}

renderFileUpload(result)
return nil
},
}

// renderFileUpload prints a human-friendly summary of a file_upload object.
// Kept as a standalone helper so it can be reused by future commands (for
// example a --watch flag on `file upload` that polls status via 'file get').
func renderFileUpload(result map[string]interface{}) {
name, _ := result["filename"].(string)
if name == "" {
name, _ = result["name"].(string)
}
id, _ := result["id"].(string)
status, _ := result["status"].(string)
contentType, _ := result["content_type"].(string)
created, _ := result["created_time"].(string)
expires, _ := result["expiry_time"].(string)

var size int64
switch v := result["content_length"].(type) {
case float64:
size = int64(v)
case int64:
size = v
}

title := "File upload"
if name != "" {
title = fmt.Sprintf("File upload: %s", name)
}
render.Title("✓", title)
render.Field("ID", id)
if status != "" {
render.Field("Status", status)
}
if size > 0 {
render.Field("Size", fmt.Sprintf("%d bytes", size))
}
if contentType != "" {
render.Field("Content-Type", contentType)
}
if created != "" {
if len(created) > 10 {
created = created[:10]
}
render.Field("Created", created)
}
if expires != "" {
if len(expires) > 10 {
expires = expires[:10]
}
render.Field("Expires", expires)
}
if fileURL, ok := result["file"].(map[string]interface{}); ok {
if u, _ := fileURL["url"].(string); u != "" {
render.Field("URL", u)
}
}
}

func init() {
fileUploadCmd.Flags().String("to", "", "Target page ID to attach file to")
fileUploadCmd.Flags().String("name", "", "Override filename (required for stdin source, optional for URL)")
fileCmd.AddCommand(fileListCmd)
fileCmd.AddCommand(fileUploadCmd)
fileCmd.AddCommand(fileGetCmd)
}
78 changes: 78 additions & 0 deletions cmd/file_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cmd

import (
"bytes"
"io"
"os"
"strings"
"testing"
)

func TestRenderFileUpload_AllFields(t *testing.T) {
// Redirect stdout so we can assert on the rendered lines.
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

renderFileUpload(map[string]interface{}{
"id": "upload-abc",
"filename": "chart.png",
"status": "uploaded",
"content_type": "image/png",
"content_length": float64(45231),
"created_time": "2026-04-30T05:00:00.000Z",
"expiry_time": "2026-05-30T05:00:00.000Z",
"file": map[string]interface{}{
"url": "https://notion.s3/chart.png?sig=x",
},
})

w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
out := buf.String()

for _, want := range []string{
// Title goes through fatih/color which bypasses our stdout pipe,
// so we only assert on fields whose VALUES come from fmt.Println.
"upload-abc",
"uploaded",
"image/png",
"45231 bytes",
"2026-04-30",
"2026-05-30",
"https://notion.s3/chart.png",
} {
if !strings.Contains(out, want) {
t.Errorf("missing %q in output:\n%s", want, out)
}
}
}

func TestRenderFileUpload_MinimalPending(t *testing.T) {
// Only the bare minimum — a pending upload that hasn't received bytes yet.
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

renderFileUpload(map[string]interface{}{
"id": "upload-pending",
"status": "pending",
})

w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
io.Copy(&buf, r)
out := buf.String()

if !strings.Contains(out, "upload-pending") || !strings.Contains(out, "pending") {
t.Errorf("minimal render missing fields:\n%s", out)
}
// Should NOT print empty Size / Created / Expires / Content-Type values.
// We check for the *value pattern* rather than the key (which goes through color lib).
if strings.Contains(out, "bytes") {
t.Errorf("unexpected size line for pending object:\n%s", out)
}
}
Loading