> Upload files and create download URLs from GraphQL, Node, and Next.js

# Uploading & Downloading

The SDKs wrap the whole [upload flow](/files/overview) in one call: `client.files.upload` on the
server, `usePyloUpload` in the browser. In the browser the bytes stream straight to Pylo, so you
need no extra API route and no file bytes cross your server. The hook reports byte-accurate progress
and error state, and it can abort a batch.

## Uploading
**GraphQL**

```graphql filename="upload.graphql"
# step 1: request an upload URL

mutation {
  createUpload(input: { entity_relation_path: "Contact.avatar" }) {
    id   # the future PyloMedia id
    url  # step 2: POST the file here as multipart/form-data
  }
}
```
**Node**

```typescript filename="upload.ts"
import { pylo } from './lib/pylo';
import { readFile } from 'node:fs/promises';

const buffer = await readFile('./avatar.png');

const media = await pylo.files.upload(buffer, {
  fileName: 'avatar.png',
  mimeType: 'image/png',
  entityRelationPath: 'contact.avatar',
});

// media: { id, fileName, mimeType, size }
```
**Next.js Server**

```typescript filename="app/actions.ts"
'use server';

import { pylo } from '@/lib/pylo';

export async function uploadAvatar(formData: FormData) {
  const file = formData.get('file') as File;

  const media = await pylo.files.upload(file, {
    entityRelationPath: 'contact.avatar',
  });

  return media.id;
}
```
**Next.js Client**

```tsx filename="avatar-uploader.tsx"
'use client';

import { pylo } from "@/lib/pylo-hooks";

export function AvatarUploader() {
  const { startUpload, isUploading, progress, error } = pylo.usePyloUpload({
    entityRelationPath: 'contact.avatar',
  });

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) startUpload(file);
  };

  return (
    <div>
      <input type="file" disabled={isUploading} onChange={onChange} />
      {isUploading && <progress value={progress} max={100} />}
      {error && <p>{error.message}</p>}
    </div>
  );
}
```

Accepted sources on the server are `File`, `Blob`, `ArrayBuffer`, and typed arrays (including Node
`Buffer`). `fileName` is required unless the source is a `File`. Passing `entityRelationPath`
enforces the relation's mime and extension rules at upload time; omit it for unrestricted,
unattached uploads. `isPublic: true` creates a permanent public download URL.

## Attaching to a record

`attachTo` connects the uploaded file in the same call. Use `mode: 'add'` for list relations. On the
client it also invalidates the entity's queries, so open views refetch.
**GraphQL**

```graphql filename="attach.graphql"
# after the upload, connect the PyloMedia id via upsert
mutation {
  updateContact(input: {
    id: "contact-uuid"
    avatar_set: { id: "media-uuid" }
  }) {
    data { id }
  }
}
```
**Node**

```typescript filename="attach.ts"
await pylo.files.upload(buffer, {
  fileName: 'avatar.png',
  entityRelationPath: 'contact.avatar',
  attachTo: { id: contactId },
});

// list relations: attachTo: { id: projectId, mode: 'add' }
```
**Next.js Server**

```typescript filename="attach.ts"
await pylo.files.upload(file, {
  entityRelationPath: 'contact.avatar',
  attachTo: { id: contactId },
});

// list relations: attachTo: { id: projectId, mode: 'add' }
```
**Next.js Client**

```typescript filename="attach.ts"
await startUpload(file, { attachTo: { id: contactId } });

// list relations: attachTo: { id: projectId, mode: 'add' }
```

## Upload now, attach later

If you do not know the target record yet, upload without `attachTo`. The result always contains the
PyloMedia id; connect it later with a normal [upsert](/data/mutations):
**Node**

```typescript filename="deferred-attach.ts"
const media = await pylo.files.upload(buffer, {
  fileName: 'report.pdf',
  entityRelationPath: 'project.documents',
});

// later, once the record exists:
await pylo.project.upsert({
  id: projectId,
  documents_add: [{ id: media.id }],
});
```
**Next.js Server**

```typescript filename="deferred-attach.ts"
const media = await pylo.files.upload(file, {
  entityRelationPath: 'project.documents',
});

// later, once the record exists:
await pylo.project.upsert({
  id: projectId,
  documents_add: [{ id: media.id }],
});
```
**Next.js Client**

```typescript filename="deferred-attach.ts"
const { startUpload } = pylo.usePyloUpload({
  entityRelationPath: 'contact.avatar',
});
const { mutateAsync: upsertContact } = pylo.usePyloUpsert('contact');

const [media] = await startUpload(file);
// media: { id, fileName, mimeType, size }

// later, once the record exists:
await upsertContact({
  id: contactId,
  avatar_set: { id: media.id },
});
```

## Downloading

Private download URLs expire (about 5 minutes by default), so fetch them right before use and never
store them. Public files return their permanent URL from the same calls.
**GraphQL**

```graphql filename="download.graphql"
mutation {
  createDownload(id: "media-uuid") {
    url
  }
}
```
**Node**

```typescript filename="download.ts"
const url = await pylo.files.getDownloadUrl(mediaId);
const response = await fetch(url);
```
**Next.js Server**

```typescript filename="download.ts"
const url = await pylo.files.getDownloadUrl(mediaId);
```
**Next.js Client**

```tsx filename="avatar.tsx"
'use client';

import { pylo } from "@/lib/pylo-hooks";

export function Avatar({ mediaId }: { mediaId: string }) {
  const { data: url } = pylo.usePyloFileUrl(mediaId);
  return url ? <img src={url} alt="" /> : null;
}
```

`usePyloFileUrl` caches the URL and refetches it before the expiry. You can also select `url` on the
relation in a regular query; each fetch returns fresh URLs.

## Upload URLs

`createUploadUrl` returns the upload URL without sending bytes, for example to hand it to another
process or a language without an SDK. It expires after about 1 hour. POST the file to `url` as
`multipart/form-data` with a single file part.
**GraphQL**

```graphql filename="upload-url.graphql"
mutation {
  createUpload(input: { entity_relation_path: "Project.documents" }) {
    id
    url
  }
}
```
**Node**

```typescript filename="upload-url.ts"
const uploadUrl = await pylo.files.createUploadUrl({
  entityRelationPath: 'project.documents',
});
// uploadUrl: { id, url }
```
**Next.js Server**

```typescript filename="upload-url.ts"
const uploadUrl = await pylo.files.createUploadUrl({
  entityRelationPath: 'project.documents',
});
// uploadUrl: { id, url }
```

## usePyloUpload reference

`usePyloUpload(options?)` returns:

- `startUpload(files, overrides?)`: uploads one file or an array, resolves with the created
  PyloMedia rows (`{ id, fileName, mimeType, size }` each)
- `isUploading`: whether a batch is in flight
- `progress`: `0-100`, aggregated by bytes across the batch
- `uploadedFiles`: results of the last successful batch
- `error`: the failure of the last batch, or `null`
- `abort()`: cancels the in-flight batch
- `reset()`: clears progress, error, and results

Options (all optional; you can also pass `entityRelationPath`, `isPublic`, and `attachTo` per call,
as the second argument of `startUpload`):

- `entityRelationPath`: the media relation the files are destined for, for example
  `'contact.avatar'` (autocompletes from your schema)
- `isPublic`: create permanent public download URLs (requires `entityRelationPath`)
- `attachTo`: `{ id, mode? }`, connects each uploaded file to the record; `mode` is `'set'`
  (default, single relations) or `'add'` (list relations)
- `onProgress`, `onSuccess`, `onError`: callbacks
- `headers`: extra headers for the GraphQL requests

> **Progress reporting**
> In the browser, progress is byte-accurate. In Node, `onProgress` fires only at the start and the
> end of the transfer, because the runtime's `fetch` emits no upload progress events.
