Skip to content

src/ensure-shared-direct-url.ts

227 lines · 7.8 KB · TypeScript
Raw
/**
 * Ensure a Dropbox shared link exists for a path and return both the share URL
 * and a browser/curl-friendly direct content URL on `dl.dropboxusercontent.com` (`raw=1`).
 * Reuses an existing link via `sharing/list_shared_links` when create returns
 * “link already exists”. Prefer this over hand-rolling share + URL rewriting.
 * Requires `confirm: true` (create may mutate); `dryRun: true` previews create.
 *
 * @param params.path - Dropbox path starting with `/` (prefer `/Apps/Kody/...`).
 * @param params.settings - Optional shared-link settings for create.
 * @param params.confirm - Required `true` to create or list/reuse a link.
 * @param params.dryRun - When true, return the planned create request without calling Dropbox.
 * @returns `{ path, url, directUrl, reused, link }` — `directUrl` is curl-friendly content.
 *
 * @example
 * import ensureSharedDirectUrl from 'kody:@kentcdodds/dropbox/ensure-shared-direct-url'
 * const result = await ensureSharedDirectUrl({
 *   path: '/Apps/Kody/notes/guide.pdf',
 *   confirm: true,
 * })
 * // => { path, url, directUrl, reused, link }
 */

import { DropboxApiError, dropboxRpc, request } from './request.ts'
import { APP_FOLDER_ROOT, suggestAppFolderPath } from './paths.ts'
import type { JsonRecord } from './types.ts'
import { requireDropboxPath, requireRecord } from './types.ts'

export type EnsureSharedDirectUrlInput = {
	path: string
	settings?: JsonRecord
	confirm?: boolean
	dryRun?: boolean
}

export type EnsureSharedDirectUrlResult = {
	path: string
	url: string
	directUrl: string
	reused: boolean
	link: JsonRecord
}

/**
 * Ensure a Dropbox shared link exists for a path and return both the share URL
 * and a browser/curl-friendly direct content URL on `dl.dropboxusercontent.com` (`raw=1`).
 * Reuses an existing link via `sharing/list_shared_links` when create returns
 * “link already exists”. Prefer this over hand-rolling share + URL rewriting.
 * Requires `confirm: true` (create may mutate); `dryRun: true` previews create.
 *
 * @param params.path - Dropbox path starting with `/` (prefer `/Apps/Kody/...`).
 * @param params.settings - Optional shared-link settings for create.
 * @param params.confirm - Required `true` to create or list/reuse a link.
 * @param params.dryRun - When true, return the planned create request without calling Dropbox.
 * @returns `{ path, url, directUrl, reused, link }` — `directUrl` is curl-friendly content.
 *
 * @example
 * import ensureSharedDirectUrl from 'kody:@kentcdodds/dropbox/ensure-shared-direct-url'
 * const result = await ensureSharedDirectUrl({
 *   path: '/Apps/Kody/notes/guide.pdf',
 *   confirm: true,
 * })
 * // => { path, url, directUrl, reused, link }
 */
export async function ensureSharedDirectUrl(
	input: EnsureSharedDirectUrlInput,
): Promise<EnsureSharedDirectUrlResult | JsonRecord> {
	const path = requireDropboxPath(input.path)
	if (
		input.settings !== undefined &&
		(!input.settings || typeof input.settings !== 'object' || Array.isArray(input.settings))
	) {
		throw new Error('settings must be an object when provided.')
	}

	if (input.dryRun) {
		return {
			dryRun: true,
			endpoint: 'sharing/create_shared_link_with_settings',
			path,
			body: {
				path,
				...(input.settings ? { settings: input.settings } : {}),
			},
			onAlreadyExists: 'sharing/list_shared_links { path, direct_only: true }',
			directUrlHint: 'Rewrite the share host to dl.dropboxusercontent.com and set query param raw=1.',
			authorization: '[managed dropbox OAuth integration]',
		}
	}

	if (input.confirm !== true) {
		throw new Error(
			'ensure-shared-direct-url may create a shared link. Pass confirm: true only after explicit user approval, or use dryRun: true.',
		)
	}

	try {
		const created = await request({
			endpoint: 'sharing/create_shared_link_with_settings',
			body: {
				path,
				...(input.settings ? { settings: input.settings } : {}),
			},
			confirm: true,
		})
		const link = unwrapLink(created)
		const url = requireUrl(link)
		return {
			path,
			url,
			directUrl: toDirectContentUrl(url),
			reused: false,
			link,
		}
	} catch (error) {
		if (!isSharedLinkAlreadyExists(error)) {
			throw enhancePathError(error, path)
		}
		const link = await findExistingDirectLink(path)
		const url = requireUrl(link)
		return {
			path,
			url,
			directUrl: toDirectContentUrl(url),
			reused: true,
			link,
		}
	}
}

async function findExistingDirectLink(path: string): Promise<JsonRecord> {
	const listed = await dropboxRpc<{ links?: JsonRecord[]; has_more?: boolean; cursor?: string }>(
		'sharing/list_shared_links',
		{ path, direct_only: true },
	)
	const links = Array.isArray(listed.links) ? listed.links : []
	const match =
		links.find((link) => typeof link?.url === 'string' && link.url.length > 0) ?? links[0]
	if (!match || typeof match.url !== 'string') {
		throw new Error(
			`Shared link already exists for ${path}, but list_shared_links returned no usable url.`,
		)
	}
	return match
}

/** Convert a Dropbox share URL into a direct content URL (raw=1; follows redirects to content). */
export function toDirectContentUrl(shareUrl: string): string {
	let parsed: URL
	try {
		parsed = new URL(shareUrl)
	} catch {
		throw new Error('Shared link url is not a valid URL.')
	}
	// Prefer the content host for programmatic/curl access; remove conflicting dl.
	parsed.hostname = 'dl.dropboxusercontent.com'
	parsed.searchParams.delete('dl')
	parsed.searchParams.set('raw', '1')
	return parsed.toString()
}

function unwrapLink(result: unknown): JsonRecord {
	if (result && typeof result === 'object' && 'body' in result && 'ok' in result) {
		const body = (result as { body: unknown }).body
		if (!body || typeof body !== 'object' || Array.isArray(body)) {
			throw new Error('Unexpected create_shared_link response body.')
		}
		return body as JsonRecord
	}
	if (!result || typeof result !== 'object' || Array.isArray(result)) {
		throw new Error('Unexpected create_shared_link response.')
	}
	return result as JsonRecord
}

function requireUrl(link: JsonRecord): string {
	if (typeof link.url !== 'string' || !link.url) {
		throw new Error('Shared link response did not include url.')
	}
	return link.url
}

function isSharedLinkAlreadyExists(error: unknown): boolean {
	if (!(error instanceof DropboxApiError)) return false
	const blob = `${error.message} ${JSON.stringify(error.details ?? '')}`
	return /shared_link_already_exists/i.test(blob)
}

function enhancePathError(error: unknown, path: string): Error {
	if (!(error instanceof DropboxApiError)) {
		return error instanceof Error ? error : new Error(String(error))
	}
	const details = JSON.stringify(error.details ?? '')
	const looksLikePath =
		error.status === 409 ||
		/path\/not_found|path\/malformed|not_found/i.test(details) ||
		/path\/not_found|path\/malformed|not_found/i.test(error.message)
	if (!looksLikePath) return error
	const suggestion = suggestAppFolderPath(path)
	const hint =
		suggestion !== path
			? ` Path may need the Kody app-folder prefix. Try "${suggestion}" (APP_FOLDER_ROOT is ${APP_FOLDER_ROOT}).`
			: ` Writable Kody files usually live under ${APP_FOLDER_ROOT}/….`
	return new DropboxApiError(`${error.message}.${hint}`, {
		status: error.status,
		statusText: error.statusText,
		requestId: error.requestId,
		details: error.details,
	})
}

/**
 * Ensure a shared link and return `{ path, url, directUrl }`.
 * Requires `confirm: true`; use `dryRun: true` to preview.
 * @example
 * import ensureSharedDirectUrl from 'kody:@kentcdodds/dropbox/ensure-shared-direct-url'
 * const result = await ensureSharedDirectUrl({
 *   path: '/Apps/Kody/notes/guide.pdf',
 *   confirm: true,
 * })
 */
export default async function ensureSharedDirectUrlEntrypoint(
	params: Partial<EnsureSharedDirectUrlInput> & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'ensure-shared-direct-url')
	requireDropboxPath(input.path)
	return ensureSharedDirectUrl(input as unknown as EnsureSharedDirectUrlInput)
}