All files / lib download.ts

77.08% Statements 37/48
53.33% Branches 16/30
83.33% Functions 5/6
79.06% Lines 34/43

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                                                              2x 1x 1x       1x   1x     1x       1x         1x 1x                       1x           1x   1x 1x       1x       1x                   1x 1x 1x 1x 1x 1x                               2x   2x   1x     2x 9x 9x 9x   7x   9x         2x 1x 1x     1x    
import { Capacitor } from '@capacitor/core';
import { FileViewer } from '@capacitor/file-viewer';
import { Directory, Encoding, Filesystem } from '@capacitor/filesystem';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import YAML from 'yaml';
 
import { keys } from './utils.js';
 
export async function openFileFromUrl(url: URL | string) {
	if (Capacitor.isNativePlatform()) {
		await FileViewer.openDocumentFromUrl({
			url: url.toString(),
		});
	} else {
		const a = document.createElement('a');
		a.href = url.toString();
		a.download = 'true';
		a.click();
	}
}
 
/**
 * **WARNING:** On native platforms, binary data is converted to base64 before writing, which can lag the UI for large files.
 * @param contentType defaults to 'text/plain' for strings and 'application/octet-stream' for blobs
 * @returns Saved file's URI on native platforms, void on web
 */
export async function downloadAsFile(
	content: Blob | ArrayBuffer | string,
	filename: string,
	contentType: string
): Promise<URL | void> {
	if (Capacitor.isNativePlatform()) {
		const permission = await Filesystem.checkPermissions();
		Iif (permission.publicStorage !== 'granted') {
			await Filesystem.requestPermissions();
		}
 
		const { path: directoryUri } = await FilePicker.pickDirectory();
 
		console.info('Got directory URI', directoryUri);
 
		// Directory URI looks like: content://com.android.externalstorage.documents/tree/primary%3ADocuments%2Fcigale%20exports
		const directory = new URL(
			decodeURIComponent(new URL(directoryUri).pathname.split('/').at(-1) || '')
		).pathname;
 
		console.info('Saving file to', directory);
 
		/** @type {Pick<import('@capacitor/filesystem').WriteFileOptions, 'data' | 'encoding'>} */
		let input: Pick<import('@capacitor/filesystem').WriteFileOptions, 'data' | 'encoding'>;
 
		if (typeof content === 'string') {
			input = {
				data: content,
				encoding: Encoding.UTF8,
			};
		} else E{
			input = {
				data: new Uint8Array(
					content instanceof Blob ? await content.arrayBuffer() : content
				).toBase64(),
			};
		}
 
		const { uri } = await Filesystem.writeFile({
			path: `${directory.replace(/^content:\/\//, '')}/${filename}`,
			directory: Directory.ExternalStorage,
			...input,
		});
 
		console.info('File saved to', uri);
 
		try {
			return new URL(uri);
		} catch (_err) {
			// Some platforms return URIs that the URL constructor doesn't accept in Node/jsdom.
			// Fall back to returning the raw string so callers can handle it.
			return uri;
		}
	} else {
		const blob =
			content instanceof Blob
				? content
				: new Blob([content], {
						type:
							contentType ??
							(typeof content === 'string'
								? 'text/plain'
								: 'application/octet-stream'),
					});
 
		const url = URL.createObjectURL(blob);
		const a = document.createElement('a');
		a.href = url;
		a.download = filename;
		a.click();
		URL.revokeObjectURL(url);
	}
}
 
/**
 *
 * @param object the object to serialize
 * @param ordering an array of keys in target order, for the top-level object
 * @param schema the json schema URL
 */
export function stringifyWithToplevelOrdering<Keys extends string>(
	format: 'json' | 'yaml',
	schema: string,
	object: { [K in Keys]?: unknown },
	ordering: readonly Keys[]
) {
	let keysOrder = [...ordering];
 
	if (format === 'json') {
		// @ts-expect-error $schema is not in Keys but we add it anyways
		keysOrder = ['$schema', ...keysOrder];
	}
 
	const reviver = (_: unknown, value: unknown) => {
		Iif (value === null) return value;
		Iif (Array.isArray(value)) return value;
		if (typeof value !== 'object') return value;
 
		Eif (keys(value).every((key) => keysOrder.includes(key))) {
			// @ts-expect-error key in value thanks to if check above
			return Object.fromEntries(keysOrder.map((key) => [key, value[key]]));
		}
		return value;
	};
 
	if (format === 'yaml') {
		const yamled = YAML.stringify(object, reviver, 2);
		return `# yaml-language-server: $schema=${schema}\n\n${yamled}`;
	}
 
	return JSON.stringify({ $schema: schema, ...object }, reviver, 2);
}