# Store files

Keep uploads such as artwork and photos in the merchant’s Shopify Files, uploaded by the browser straight to Shopify.

Source: https://littleworks.app/docs/files

Files live in the merchant’s own **Shopify Files** (Content → Files in Shopify admin), not in Littleworks. The browser uploads a file directly to Shopify, so large bodies never pass through a work. A work only asks Shopify for an upload target and then adds the uploaded file to Files. Records keep the file’s ID.

- `await shopify.files.createUpload({filename, mimeType, fileSize})` returns `{url, method, parameters, resourceUrl, contentType}`: a one-time target the browser POSTs the file to.
- `await shopify.files.save({resourceUrl, contentType?, alt?, filename?})` adds the uploaded file to Files and returns it.
- `await shopify.files.get(id)` returns the file’s current status and URL, or `null` when it no longer exists.
- `await shopify.files.delete(id)` permanently deletes the file and returns `false` when it did not exist.

`save` and `get` return `{id, type, status, url, alt, mimeType, size, width, height, createdAt, errors}`. `type` is `IMAGE`, `FILE` or `VIDEO`, chosen from the MIME type: JPEG, PNG, GIF, WebP and HEIC are images; MP4, MOV and WebM are videos; any other type except HTML is a file. Shopify processes files after `save`, so `status` starts as `UPLOADED` or `PROCESSING` and `url` can be `null` until it is `READY`. A `FAILED` file lists why in `errors`.

## Access

The helpers are ordinary [Admin GraphQL](https://littleworks.app/docs/shopify) calls made for you, so the work needs `permissions.shopify: true`, and the store must grant Files access in Littleworks → Agent access → Permissions. Uploading, saving and deleting need **Edit** (`write_files`); `get` needs viewing (`read_files`). Without it, the helpers throw `SHOPIFY_FILES_ACCESS_REQUIRED` with that instruction. Page data works can call `get` but not the others, since they only read.

## Example: artwork for a quote request

The first work hands the browser an upload target. Its input schema sets the types and size the store accepts.

### quotes.artwork-upload

```javascript
export default async ({ input, shopify }) => {
  return shopify.files.createUpload({
    filename: input.filename,
    mimeType: input.mimeType,
    fileSize: input.fileSize,
  });
};
```

### Manifest

```json
{
  "name": "quotes.artwork-upload",
  "title": "Prepare an artwork upload",
  "feature": "quotes",
  "trigger": {
    "type": "http",
    "method": "POST",
    "auth": "public",
    "origins": [
      "https://your-store.example"
    ]
  },
  "permissions": {
    "shopify": true
  },
  "inputSchema": {
    "type": "object",
    "properties": {
      "filename": {
        "type": "string",
        "minLength": 1,
        "maxLength": 200
      },
      "mimeType": {
        "enum": [
          "image/png",
          "image/jpeg",
          "application/pdf"
        ]
      },
      "fileSize": {
        "type": "integer",
        "minimum": 1,
        "maximum": 10000000
      }
    },
    "required": [
      "filename",
      "mimeType",
      "fileSize"
    ],
    "additionalProperties": false
  }
}
```

The browser sends the file to Shopify as a form: every returned parameter, then the file last, in a field named `file`. It then submits the request with the `resourceUrl`.

### Upload from the storefront

```javascript
const file = form.querySelector('input[type="file"]').files[0];
const post = async (url, body) => {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!response.ok) throw new Error("The request could not be completed");
  return response.json();
};

const target = await post(uploadWorkUrl, {
  filename: file.name,
  mimeType: file.type,
  fileSize: file.size,
});

const upload = new FormData();
for (const { name, value } of target.parameters) upload.append(name, value);
upload.append("file", file);
const sent = await fetch(target.url, { method: target.method, body: upload });
if (!sent.ok) throw new Error("The file could not be uploaded");

await post(submitWorkUrl, {
  name: form.elements.name.value,
  details: form.elements.details.value,
  artwork: { resourceUrl: target.resourceUrl, contentType: target.contentType },
});
```

The second work saves the file to Files and keeps its ID on the record. Its schema accepts only an HTTPS `resourceUrl`, and `save` also rejects anything that is not a Shopify upload target, so a visitor cannot make the store import a file from elsewhere.

### quotes.submit

```javascript
export default async ({ input, db, shopify }) => {
  const file = await shopify.files.save({
    resourceUrl: input.artwork.resourceUrl,
    contentType: input.artwork.contentType,
    alt: `Artwork for ${input.name}`,
  });

  const quote = await db.collection("quotes").create({
    name: input.name,
    details: input.details,
    artworkFileId: file.id,
    status: "new",
  });

  return { id: quote.id };
};
```

### Input schema

```json
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "details": {
      "type": "string",
      "maxLength": 2000
    },
    "artwork": {
      "type": "object",
      "properties": {
        "resourceUrl": {
          "type": "string",
          "pattern": "^https://",
          "maxLength": 2048
        },
        "contentType": {
          "enum": [
            "IMAGE",
            "FILE"
          ]
        }
      },
      "required": [
        "resourceUrl",
        "contentType"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "name",
    "artwork"
  ],
  "additionalProperties": false
}
```

To show the artwork, a page data work or another work calls `shopify.files.get(quote.data.artworkFileId)` and uses `url` once `status` is `READY`. Image and file URLs are on Shopify’s CDN, so a storefront or admin page can display or link to them directly. To render a file from Liquid instead, save its ID to a `file_reference` metafield and use Shopify’s `image_url` filter or the file’s `url`.

## Limits and care

- Shopify accepts images and other files up to 20 MB and videos up to 1 GB, and never HTML. `createUpload` rejects larger sizes before asking Shopify. `fileSize` is what the browser reports; for a stricter limit, also check `size` once the file is `READY` and delete files that exceed it.
- Files are public to anyone with their URL. Do not use them for confidential documents.
- Each helper call is one SDK call within the run’s limit. They do not count as external requests.
- Test runs save real files to the store. `delete_test_records` removes test records but not the files they reference, so delete test files with `shopify.files.delete`.
- Deleting is permanent and removes the file wherever the store uses it, including products and pages.
