All files / lib download.js

88.09% Statements 37/42
57.14% Branches 16/28
100% Functions 5/5
91.89% Lines 34/37

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                          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 { Directory, Encoding, Filesystem } from '@capacitor/filesystem';
import { FilePicker } from '@capawesome/capacitor-file-picker';
import YAML from 'yaml';
 
/**
 * **WARNING:** On native platforms, binary data is converted to base64 before writing, which can lag the UI for large files.
 * @param {Blob | ArrayBuffer | string} content
 * @param {string} filename
 * @param {string} [contentType] defaults to 'text/plain' for strings and 'application/octet-stream' for blobs
 * @returns {Promise<URL|void>} Saved file's URI on native platforms, void on web
 */
export async function downloadAsFile(content, filename, contentType) {
	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;
 
		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);
	}
}
 
/**
 *
 * @template {string} Keys
 * @param { { [K in Keys]?: unknown } } object the object to serialize
 * @param {readonly Keys[]} ordering an array of keys in target order, for the top-level object
 * @param {'json' | 'yaml'} format
 * @param {string} schema the json schema URL
 */
export function stringifyWithToplevelOrdering(format, schema, object, ordering) {
	let keysOrder = [...ordering];
 
	if (format === 'json') {
		// @ts-expect-error
		keysOrder = ['$schema', ...keysOrder];
	}
 
	/**
	 * @param {*} _
	 * @param {*} value
	 */
	const reviver = (_, value) => {
		Iif (value === null) return value;
		Iif (Array.isArray(value)) return value;
		if (typeof value !== 'object') return value;
 
		// @ts-expect-error
		Eif (Object.keys(value).every((key) => keysOrder.includes(key))) {
			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);
}