> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ns.rocks/llms.txt
> Use this file to discover all available pages before exploring further.

# Uploads

> Upload files and directories with direct or resumable transfer.

Use `write()` for in-memory strings and bytes. Use `upload_file()` for local
files, `upload_dir()` for directories, and `upload()` when you want the SDK to
dispatch by source type.

`upload()` accepts local file paths, directories, file-like objects, and stdin
style streams. Directory-specific options such as `conflict` and
`ignore_patterns` belong on `upload_dir()`; resumable options such as
`resumable`, `checksum`, and `spool_to_disk` belong on `upload_file()`.

## Write many small files

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  machine.files.write_files([
      ("/workspace/app.py", "print('hello')\n"),
      ("/workspace/README.md", "# Demo\n"),
  ])
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace machine file write-files mch_... '[["/workspace/app.py","print(\"hello\")\n"],["/workspace/README.md","# Demo\n"]]'
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  # batch-write is a multipart request: a JSON `manifest` part plus one part per file field.
  printf "print('hello')\n" > app.py
  printf '# Demo\n' > README.md
  curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/files/batch-write" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -F 'manifest={"files":[{"path":"/workspace/app.py","field":"file0"},{"path":"/workspace/README.md","field":"file1"}]}' \
    -F "file0=@app.py" \
    -F "file1=@README.md"
  ```
</CodeGroup>

## Upload one file

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  result = machine.files.upload_file("./dist/app.tar.gz", "/workspace/app.tar.gz")
  print(result.transport, result.target_path, result.bytes_uploaded)
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace machine upload mch_... ./dist/app.tar.gz /workspace/app.tar.gz
  # stream from stdin with "-" as the source:
  cat ./dist/app.tar.gz | nullspace machine upload mch_... - /workspace/app.tar.gz
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  # 1. Mint a signed upload URL for the destination path.
  URL=$(curl -fsS -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/files/upload-url" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"path": "/workspace/app.tar.gz"}' | jq -r .url)

  # 2. Stream the file bytes to the signed URL with PUT.
  curl -X PUT "${URL}" \
    -H "Content-Type: application/octet-stream" \
    --data-binary @./dist/app.tar.gz
  ```
</CodeGroup>

Upload destinations must be valid machine paths. Do not upload into
`/workspace/.nullspace`, which is reserved for runtime metadata and resumable
transfer state.

## Upload a directory

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  result = machine.files.upload_dir(
      "./src",
      "/workspace/src",
      conflict="merge",
      ignore_patterns=["*.pyc", "!pkg/__init__.py"],
  )
  print(result.file_count, result.target_path)
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace machine upload mch_... ./src /workspace/src --exclude '*.pyc'
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  # Directory uploads stream a tar archive through a resumable upload session.
  # 1. Create the session (kind directory_tar).
  tar -czf src.tar.gz ./src
  curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"kind": "directory_tar", "source_kind": "directory_archive", "target_path": "/workspace/src", "content_length": '"$(wc -c < src.tar.gz)"', "conflict": "merge", "archive_format": "tar", "checksum_algorithm": "sha256"}'

  # 2. PUT each part (here a single part 0) to the returned upload id.
  curl -X PUT "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads/upload_123/parts/0" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/octet-stream" \
    --data-binary @src.tar.gz

  # 3. Complete to extract and publish the archive.
  curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads/upload_123/complete" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}"
  ```
</CodeGroup>

Directory uploads honor `.nullspaceignore` from the source root and append any
explicit `ignore_patterns` after it. Conflict policies control how existing
destination files are handled; use `"merge"` for normal sync-style uploads and
choose stricter policies when automation must fail on pre-existing paths.

## Resumable upload and progress

<CodeGroup>
  ```python Python SDK theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  from nullspace import FileUploadError

  def on_progress(event) -> None:
      print(event.phase, event.bytes_completed, event.bytes_total, event.transport)

  try:
      result = machine.files.upload_file(
          "./large-model.bin",
          "/data/model.bin",
          resumable=True,
          checksum="auto",
          spool_to_disk="auto",
          max_concurrency=4,
          progress=on_progress,
      )
  except FileUploadError as exc:
      if exc.upload_id is None:
          raise
      result = machine.files.resume_upload(
          exc.upload_id,
          "./large-model.bin",
          progress=on_progress,
      )

  print(result.upload_id, result.bytes_uploaded)
  ```

  ```bash CLI theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  nullspace machine upload mch_... ./large.bin /data/large.bin \
    --resumable always --checksum auto --concurrency 4
  # resume an interrupted transfer by upload id:
  nullspace machine upload mch_... ./large.bin /data/large.bin --resume upload_...
  ```

  ```bash HTTP API theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
  # Resumable file uploads use the same upload-session primitives.
  # 1. Create the session (kind file, source restartable_file).
  curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{"kind": "file", "source_kind": "restartable_file", "target_path": "/data/model.bin", "content_length": 1048576, "conflict": "merge", "checksum_algorithm": "sha256"}'

  # 2. Upload each part; parts may be replayed and sent out of order.
  curl -X PUT "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads/upload_123/parts/0" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}" \
    -H "Content-Type: application/octet-stream" \
    --data-binary @./large-model.bin

  # 3. Poll status to resume after an interruption, then complete.
  curl -X GET "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads/upload_123" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}"
  curl -X POST "${NULLSPACE_API_URL}/v1/machines/mch_123/uploads/upload_123/complete" \
    -H "Authorization: Bearer ${NULLSPACE_API_KEY}"
  ```
</CodeGroup>

## Signed upload URL

```python theme={"theme":{"light":"one-light","dark":"one-dark-pro"}}
grant = machine.files.upload_url("/workspace/input.bin")
print(grant.url, grant.method, grant.headers)
```

Use a signed upload URL when a browser or another service should upload bytes
directly. Use `machine.upload_url(path)` as a convenience wrapper around
`machine.files.upload_url(path).url`.

The signed upload grant includes the URL plus request metadata such as the HTTP
method and required headers. Forward those fields to the client performing the
direct upload.

## Related

* [Downloads](./downloads)
* [Resumable upload example](../examples/resumable-upload)
* [API Reference](../api-reference)
