# Dropbox Client

Upload, download, and manage files in Dropbox.

## Methods

| Method                                   | Description                                         |
| ---------------------------------------- | --------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Dropbox API request with schema validation |

## Usage

### List Files in a Folder

```typescript
import { api, z, dropbox } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_DROPBOX = "a1b2c3d4-5678-90ab-cdef-dropbox00001";

const FileMetadataSchema = z.object({
  ".tag": z.enum(["file", "folder", "deleted"]),
  name: z.string(),
  path_lower: z.string(),
  path_display: z.string(),
  id: z.string(),
  client_modified: z.string().optional(),
  server_modified: z.string().optional(),
  size: z.number().optional(),
  content_hash: z.string().optional(),
});

const ListFolderResponseSchema = z.object({
  entries: z.array(FileMetadataSchema),
  cursor: z.string(),
  has_more: z.boolean(),
});

export default api({
  name: "DropboxExample",
  integrations: {
    dropbox: dropbox(PROD_DROPBOX),
  },
  input: z.object({
    path: z.string().default(""),
  }),
  output: z.object({
    files: z.array(
      z.object({
        name: z.string(),
        path: z.string(),
        type: z.string(),
        size: z.number().optional(),
      }),
    ),
  }),
  async run(ctx, { path }) {
    const result = await ctx.integrations.dropbox.apiRequest(
      {
        method: "POST",
        path: "/2/files/list_folder",
        body: {
          path: path || "",
          recursive: false,
          include_deleted: false,
          include_has_explicit_shared_members: false,
          limit: 100,
        },
      },
      { response: ListFolderResponseSchema },
    );

    return {
      files: result.entries.map((entry) => ({
        name: entry.name,
        path: entry.path_display,
        type: entry[".tag"],
        size: entry.size,
      })),
    };
  },
});
```

### Get File Metadata

```typescript
const metadata = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/get_metadata",
    body: {
      path: "/Documents/report.pdf",
      include_media_info: true,
      include_deleted: false,
      include_has_explicit_shared_members: true,
    },
  },
  { response: FileMetadataSchema },
);

console.log(`File: ${metadata.name}, Size: ${metadata.size} bytes`);
```

### Download a File

```typescript
const DownloadResponseSchema = z.object({
  content: z.string(), // Base64 encoded or raw content
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/download",
    headers: {
      "Dropbox-API-Arg": JSON.stringify({
        path: "/Documents/report.pdf",
      }),
    },
  },
  { response: DownloadResponseSchema },
);
```

### Upload a File

```typescript
const UploadResponseSchema = z.object({
  name: z.string(),
  path_lower: z.string(),
  path_display: z.string(),
  id: z.string(),
  client_modified: z.string(),
  server_modified: z.string(),
  size: z.number(),
  content_hash: z.string(),
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/upload",
    headers: {
      "Dropbox-API-Arg": JSON.stringify({
        path: "/Documents/new-file.txt",
        mode: "add", // add, overwrite, or update
        autorename: true,
        mute: false,
      }),
      "Content-Type": "application/octet-stream",
    },
    body: "File content here",
  },
  { response: UploadResponseSchema },
);

console.log(`Uploaded: ${result.path_display}`);
```

### Create a Folder

```typescript
const CreateFolderResponseSchema = z.object({
  metadata: z.object({
    name: z.string(),
    path_lower: z.string(),
    path_display: z.string(),
    id: z.string(),
  }),
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/create_folder_v2",
    body: {
      path: "/Documents/New Folder",
      autorename: false,
    },
  },
  { response: CreateFolderResponseSchema },
);
```

### Move a File or Folder

```typescript
const MoveResponseSchema = z.object({
  metadata: FileMetadataSchema,
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/move_v2",
    body: {
      from_path: "/Documents/old-location/file.txt",
      to_path: "/Documents/new-location/file.txt",
      autorename: false,
      allow_ownership_transfer: false,
    },
  },
  { response: MoveResponseSchema },
);
```

### Copy a File or Folder

```typescript
const CopyResponseSchema = z.object({
  metadata: FileMetadataSchema,
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/copy_v2",
    body: {
      from_path: "/Documents/original.pdf",
      to_path: "/Documents/copy-of-original.pdf",
      autorename: true,
    },
  },
  { response: CopyResponseSchema },
);
```

### Delete a File or Folder

```typescript
const DeleteResponseSchema = z.object({
  metadata: FileMetadataSchema,
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/delete_v2",
    body: {
      path: "/Documents/file-to-delete.txt",
    },
  },
  { response: DeleteResponseSchema },
);
```

### Search Files

```typescript
const SearchMatchSchema = z.object({
  match_type: z.object({ ".tag": z.string() }),
  metadata: z.object({
    ".tag": z.literal("metadata"),
    metadata: FileMetadataSchema,
  }),
});

const SearchResponseSchema = z.object({
  matches: z.array(SearchMatchSchema),
  more: z.boolean(),
  cursor: z.string().optional(),
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/files/search_v2",
    body: {
      query: "quarterly report",
      options: {
        path: "/Documents",
        max_results: 25,
        file_status: "active",
        filename_only: false,
      },
    },
  },
  { response: SearchResponseSchema },
);

result.matches.forEach((match) => {
  console.log(`Found: ${match.metadata.metadata.name}`);
});
```

### Create a Shared Link

```typescript
const SharedLinkSchema = z.object({
  url: z.string(),
  name: z.string(),
  path_lower: z.string(),
  link_permissions: z.object({
    can_revoke: z.boolean(),
    resolved_visibility: z.object({ ".tag": z.string() }),
  }),
});

const result = await ctx.integrations.dropbox.apiRequest(
  {
    method: "POST",
    path: "/2/sharing/create_shared_link_with_settings",
    body: {
      path: "/Documents/report.pdf",
      settings: {
        requested_visibility: "public", // public, team_only, password
        audience: "public",
        access: "viewer", // viewer, editor, max
      },
    },
  },
  { response: SharedLinkSchema },
);

console.log(`Share URL: ${result.url}`);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await dropbox.listFolder({ ... });
await dropbox.uploadFile({ ... });

// CORRECT - Use apiRequest
await ctx.integrations.dropbox.apiRequest(
  { method: "POST", path: "/2/files/list_folder", body: { ... } },
  { response: ListFolderResponseSchema }
);
```

### API Uses POST for Most Operations

Unlike REST conventions, Dropbox uses POST for most read operations:

```typescript
// WRONG - Using GET
await ctx.integrations.dropbox.apiRequest(
  { method: "GET", path: "/2/files/list_folder", ... },
  { response: ListFolderResponseSchema }
);

// CORRECT - Use POST
await ctx.integrations.dropbox.apiRequest(
  { method: "POST", path: "/2/files/list_folder", body: { ... } },
  { response: ListFolderResponseSchema }
);
```

### Path Format

Paths must start with `/` or be empty string for root:

```typescript
// WRONG - Missing leading slash
const path = "Documents/file.txt";

// CORRECT - Leading slash required
const path = "/Documents/file.txt";

// CORRECT - Root folder is empty string
const path = "";
```

### Upload Headers

File uploads require special headers:

```typescript
// Upload requires Dropbox-API-Arg header
const headers = {
  "Dropbox-API-Arg": JSON.stringify({
    path: "/path/to/file.txt",
    mode: "add",
  }),
  "Content-Type": "application/octet-stream",
};

// The body is raw file content, not JSON
const body = "Raw file content";
```

### Download Headers

Downloads also use special headers:

```typescript
// Download parameters go in header, not body
const headers = {
  "Dropbox-API-Arg": JSON.stringify({
    path: "/path/to/file.txt",
  }),
};
```

### Entry Tags

Dropbox uses `.tag` to indicate entry type:

```typescript
const schema = z.object({
  ".tag": z.enum(["file", "folder", "deleted"]),
  // ...
});

// Check type
if (entry[".tag"] === "folder") {
  // It's a folder
}
```

### Pagination with Cursor

List operations require cursor-based continuation:

```typescript
async function listAllFiles(dropbox: DropboxClient, path: string) {
  const allEntries: FileMetadata[] = [];

  // Initial request
  let result = await ctx.integrations.dropbox.apiRequest(
    {
      method: "POST",
      path: "/2/files/list_folder",
      body: { path },
    },
    { response: ListFolderResponseSchema },
  );

  allEntries.push(...result.entries);

  // Continue with cursor
  while (result.has_more) {
    result = await ctx.integrations.dropbox.apiRequest(
      {
        method: "POST",
        path: "/2/files/list_folder/continue",
        body: { cursor: result.cursor },
      },
      { response: ListFolderResponseSchema },
    );
    allEntries.push(...result.entries);
  }

  return allEntries;
}
```

### Write Conflict Modes

Choose the right mode for uploads:

```typescript
const modes = {
  add: "Never overwrite, fail if file exists",
  overwrite: "Always overwrite existing file",
  update: "Update only if rev matches (optimistic locking)",
};

const body = {
  path: "/file.txt",
  mode: "add", // or { ".tag": "update", "update": revisionId }
};
```

### Case Sensitivity

Dropbox paths are case-insensitive but preserve case:

```typescript
// These are the same file
const path1 = "/Documents/Report.pdf";
const path2 = "/documents/report.PDF";

// Use path_lower for comparisons
if (file1.path_lower === file2.path_lower) {
  // Same file
}
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.dropbox.apiRequest(
    { method: "POST", path: "/2/files/list_folder", body: { ... } },
    { response: ListFolderResponseSchema }
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Dropbox API Documentation](https://www.dropbox.com/developers/documentation/http/documentation)
- [Files Endpoints](https://www.dropbox.com/developers/documentation/http/documentation#files)
- [Sharing Endpoints](https://www.dropbox.com/developers/documentation/http/documentation#sharing)
