Skip to content

src/upload.ts

143 lines · 4.8 KB · TypeScript
Raw
/**
 * Upload UTF-8 text or base64 bytes to Dropbox. Pick this for small files
 * (`text` or `bytesBase64` under 120 KiB binary). Larger `bytesBase64` payloads
 * auto-delegate to `./upload-large` (upload session) in the same call. Put
 * `bytesBase64` in execute `params`, never inline in `code`. Requires `confirm: true`.
 *
 * @param params.path - Destination Dropbox path starting with `/` (prefer `/Apps/Kody/...`).
 * @param params.text - UTF-8 text body (mutually exclusive with `bytesBase64`).
 * @param params.bytesBase64 - Base64 file bytes (mutually exclusive with `text`).
 * @param params.mode - `add` (default) or `overwrite`.
 * @param params.confirm - Required `true` to perform the mutating call.
 * @param params.dryRun - When true, return the planned request without calling Dropbox.
 * @returns Dropbox upload metadata, or a dry-run preview.
 *
 * @example
 * import upload from 'kody:@kentcdodds/dropbox/upload'
 * const result = await upload({
 *   path: '/Apps/Kody/notes/hello.txt',
 *   text: 'Hello',
 *   mode: 'overwrite',
 *   confirm: true,
 * })
 */

import { decodeBase64, encodeTextBase64 } from './base64.ts'
import { UPLOAD_SINGLE_MAX_BYTES } from './paths.ts'
import { request } from './request.ts'
import { uploadLarge } from './upload-large.ts'
import type { DropboxRequestResult, JsonRecord } from './types.ts'
import {
	optionalBoolean,
	optionalString,
	requireDropboxPath,
	requireRecord,
} from './types.ts'

export type UploadInput = {
	path: string
	text?: string
	bytesBase64?: string
	mode?: 'add' | 'overwrite'
	autorename?: boolean
	mute?: boolean
	strictConflict?: boolean
	confirm?: boolean
	dryRun?: boolean
}

/**
 * Upload UTF-8 text or base64 bytes to Dropbox. Pick this for small files
 * (`text` or `bytesBase64` under 120 KiB binary). Larger `bytesBase64` payloads
 * auto-delegate to `./upload-large` (upload session) in the same call. Put
 * `bytesBase64` in execute `params`, never inline in `code`. Requires `confirm: true`.
 *
 * @param params.path - Destination Dropbox path starting with `/` (prefer `/Apps/Kody/...`).
 * @param params.text - UTF-8 text body (mutually exclusive with `bytesBase64`).
 * @param params.bytesBase64 - Base64 file bytes (mutually exclusive with `text`).
 * @param params.mode - `add` (default) or `overwrite`.
 * @param params.confirm - Required `true` to perform the mutating call.
 * @param params.dryRun - When true, return the planned request without calling Dropbox.
 * @returns Dropbox upload metadata, or a dry-run preview.
 *
 * @example
 * import upload from 'kody:@kentcdodds/dropbox/upload'
 * const result = await upload({
 *   path: '/Apps/Kody/notes/hello.txt',
 *   text: 'Hello',
 *   mode: 'overwrite',
 *   confirm: true,
 * })
 */
export async function upload(
	input: UploadInput,
): Promise<DropboxRequestResult | JsonRecord> {
	const path = requireDropboxPath(input.path)
	const text = optionalString(input.text, 'text')
	const suppliedBase64 = optionalString(input.bytesBase64, 'bytesBase64')
	if ((text === undefined) === (suppliedBase64 === undefined)) {
		throw new Error('Provide exactly one of text or bytesBase64.')
	}
	const mode = input.mode ?? 'add'
	if (mode !== 'add' && mode !== 'overwrite') {
		throw new Error('mode must be "add" or "overwrite".')
	}

	const bytesBase64 = suppliedBase64 ?? encodeTextBase64(text ?? '')
	const byteLength = decodeBase64(bytesBase64).byteLength

	if (byteLength > UPLOAD_SINGLE_MAX_BYTES) {
		const delegated = await uploadLarge({
			path,
			bytesBase64,
			mode,
			autorename: input.autorename,
			mute: input.mute,
			strictConflict: input.strictConflict,
			confirm: input.confirm,
			dryRun: input.dryRun,
		})
		if (input.dryRun && delegated && typeof delegated === 'object') {
			return {
				...delegated,
				delegatedFrom: './upload',
				reason: `Decoded bytes (${byteLength}) exceed UPLOAD_SINGLE_MAX_BYTES (${UPLOAD_SINGLE_MAX_BYTES}); using upload session.`,
			}
		}
		return delegated
	}

	return request({
		endpoint: 'files/upload',
		kind: 'upload',
		apiArg: {
			path,
			mode,
			autorename: optionalBoolean(input.autorename, 'autorename') ?? false,
			mute: optionalBoolean(input.mute, 'mute') ?? false,
			strict_conflict: optionalBoolean(input.strictConflict, 'strictConflict') ?? false,
		},
		bytesBase64,
		confirm: input.confirm,
		dryRun: input.dryRun,
	})
}

/**
 * Upload UTF-8 text or base64 bytes to Dropbox.
 * Requires `confirm: true`; use `dryRun: true` to preview the request.
 * Over-threshold binary payloads auto-delegate to `./upload-large`.
 * @example
 * import upload from 'kody:@kentcdodds/dropbox/upload'
 * const result = await upload({
 *   path: '/Apps/Kody/notes/hello.txt',
 *   text: 'Hello',
 *   mode: 'overwrite',
 *   confirm: true,
 * })
 */
export default async function uploadEntrypoint(
	params: Partial<UploadInput> & Record<string, unknown> = {},
) {
	return upload(requireRecord(params, 'upload') as UploadInput)
}