Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

winget-source: migrate from Promise to async/await #119

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions winget-source/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions winget-source/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"fetch-retry": "^5.0.4",
"jszip": "^3.10.1",
"node-fetch": "^3.3.1",
"promised-sqlite3": "^2.1.0",
"sqlite3": "^5.1.5",
"winston": "^3.8.2"
},
Expand Down
82 changes: 51 additions & 31 deletions winget-source/sync-repo.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import assert from 'assert'
import async from 'async'

import { rm } from 'fs/promises'
import { EX_IOERR, EX_TEMPFAIL, EX_UNAVAILABLE } from './sysexits.js'
import { AsyncDatabase } from 'promised-sqlite3'
import { EX_IOERR, EX_OK, EX_SOFTWARE, EX_TEMPFAIL, EX_UNAVAILABLE } from './sysexits.js'

import {
buildPathpartMap,
buildURIList,
exitOnError,
exitWithCode,
extractDatabaseFromBundle,
getLocalPath,
makeTempDirectory,
Expand All @@ -17,38 +18,57 @@ import {
} from './utilities.js'


const sourceV1Filename = 'source.msix';
const sourceV2Filename = 'source2.msix';

// set up configs and temp directory
const { parallelLimit, remote, sqlite3, winston } = setupEnvironment();
const tempDirectory = await makeTempDirectory('winget-repo-');

winston.info(`start syncing with ${remote}`);

const sourceV1Filename = 'source.msix';
const sourceV2Filename = 'source2.msix';
try {
// download V1 index package to buffer
const [indexBuffer, modifiedDate, updated] = await syncFile(sourceV1Filename, true, false);
if (!updated) {
winston.info(`nothing to sync from ${remote}`);
exitWithCode(EX_OK);
}
assert(indexBuffer !== null, "Failed to get the source index buffer!");

syncFile(sourceV1Filename, true, false).catch(exitOnError(EX_UNAVAILABLE)).then(async result => {
assert(result, "Failed to catch error when syncing source index!");
const [indexBuffer, modifiedDate, synced] = result;
if (synced) {
assert(indexBuffer !== null, "Failed to get the source index buffer!");
const temp = await makeTempDirectory('winget-repo-');
const database = await extractDatabaseFromBundle(indexBuffer, temp);
const db = new sqlite3.Database(database, sqlite3.OPEN_READONLY, exitOnError(EX_IOERR));

db.all('SELECT * FROM pathparts', (error, rows) => {
const pathparts = buildPathpartMap(error, rows);
db.all('SELECT pathpart FROM manifest ORDER BY rowid DESC', (error, rows) => {
db.close();
const uris = buildURIList(error, rows, pathparts);
const download = async (uri) => await syncFile(uri, false);
async.eachLimit(uris, parallelLimit, download, (error) => {
rm(temp, { recursive: true });
exitOnError(EX_TEMPFAIL)(error);
saveFile(getLocalPath(sourceV1Filename), indexBuffer, modifiedDate).then(_ =>
syncFile(sourceV2Filename, true)
).then(_ => {
winston.info(`successfully synced with ${remote}`);
});
});
});
});
// unpack, extract and load index database
try {
const databaseFilePath = await extractDatabaseFromBundle(indexBuffer, tempDirectory);
const rawDatabase = new sqlite3.Database(databaseFilePath, sqlite3.OPEN_READONLY);

// read manifest URIs from index database
try {
const db = new AsyncDatabase(rawDatabase)
const pathparts = buildPathpartMap(await db.all('SELECT * FROM pathparts'));
const uris = buildURIList(await db.all('SELECT pathpart FROM manifest ORDER BY rowid DESC'), pathparts);
await db.close()

// sync latest manifests in parallel
try {
await async.eachLimit(uris, parallelLimit, async (uri) => await syncFile(uri, false));
} catch (error) {
exitWithCode(EX_TEMPFAIL, error);
}
} catch (error) {
exitWithCode(EX_SOFTWARE, error);
}
} catch (error) {
exitWithCode(EX_IOERR, error);
}
});

// update index packages
await saveFile(getLocalPath(sourceV1Filename), indexBuffer, modifiedDate);
await syncFile(sourceV2Filename, true);
} catch (error) {
exitWithCode(EX_UNAVAILABLE, error);
}

winston.info(`successfully synced with ${remote}`);

// clean up temp directory
await rm(tempDirectory, { recursive: true });
57 changes: 26 additions & 31 deletions winget-source/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import path from 'path'
import process from 'process'
import sqlite3 from 'sqlite3'
import winston from 'winston'

import { existsSync } from 'fs'
import { mkdir, mkdtemp, readFile, stat, utimes, writeFile } from 'fs/promises'
import { isIP } from 'net'
import { EX_IOERR, EX_SOFTWARE, EX_USAGE } from './sysexits.js'

import { EX_IOERR, EX_USAGE } from './sysexits.js'


/**
Expand Down Expand Up @@ -89,7 +91,9 @@ function getContentLength(response) {
*/
function resolvePathpart(id, pathparts) {
const pathpart = pathparts.get(id);
if (pathpart === undefined) return '';
if (pathpart === undefined) {
return '';
}
return path.posix.join(resolvePathpart(pathpart.parent, pathparts), pathpart.pathpart);
}

Expand Down Expand Up @@ -119,13 +123,11 @@ function setupWinstonLogger() {
/**
* Build a local storage for path parts from database query.
*
* @param {Error?} error Database error thrown from the query, if any.
* @param {{ rowid: number, parent: number, pathpart: string }[]} rows Rows returned by the query.
*
* @returns {Map<number, { parent: number, pathpart: string }>} In-memory path part storage to query against.
*/
export function buildPathpartMap(error, rows) {
exitOnError(EX_SOFTWARE)(error);
export function buildPathpartMap(rows) {
return new Map(rows.map(row =>
[row.rowid, { parent: row.parent, pathpart: row.pathpart }]
));
Expand All @@ -134,32 +136,29 @@ export function buildPathpartMap(error, rows) {
/**
* Build a list of all manifest URIs from database query.
*
* @param {Error?} error Database error thrown from the query, if any.
* @param {{ pathpart: string, [key: string]: string }[]} rows Rows returned by the query.
* @param {Map<number, { parent: number, pathpart: string }>} pathparts Path part storage built from the database.
*
* @returns {string[]} Manifest URIs to sync.
*/
export function buildURIList(error, rows, pathparts) {
exitOnError(EX_SOFTWARE)(error);
export function buildURIList(rows, pathparts) {
return rows.map(row => resolvePathpart(row.pathpart, pathparts));
}

/**
* Get an error handling function that logs an error and exits with given status if it occurs.
* Exit with given status with error logging.
*
* @param {number} code Exit code to use if there's an error.
* @param {number} code Exit code to use.
* @param {Error | string | null | undefined} error Error to log.
*
* @returns {(err: Error | string | null | undefined) => void} Function that handles a possible error.
* @returns {never} Exits the process.
*/
export function exitOnError(code = 1) {
return (error) => {
if (error) {
winston.exitOnError = false;
winston.error(error);
process.exit(code);
}
};
export function exitWithCode(code = 0, error = undefined) {
if (error) {
winston.exitOnError = false;
winston.error(error);
}
process.exit(code);
}

/**
Expand All @@ -171,16 +170,12 @@ export function exitOnError(code = 1) {
* @returns {Promise<string>} Path of the extracted `index.db` file.
*/
export async function extractDatabaseFromBundle(msixFile, directory) {
try {
const bundle = (msixFile instanceof Buffer) ? msixFile : await readFile(msixFile);
const zip = await JSZip.loadAsync(bundle);
const buffer = await zip.file(path.posix.join('Public', 'index.db')).async('Uint8Array');
const destination = path.join(directory, 'index.db');
await writeFile(destination, buffer);
return destination;
} catch (error) {
exitOnError(EX_IOERR)(error);
}
const bundle = (msixFile instanceof Buffer) ? msixFile : await readFile(msixFile);
const zip = await JSZip.loadAsync(bundle);
const buffer = await zip.file(path.posix.join('Public', 'index.db')).async('Uint8Array');
const destination = path.join(directory, 'index.db');
await writeFile(destination, buffer);
return destination;
}

/**
Expand Down Expand Up @@ -218,7 +213,7 @@ export async function makeTempDirectory(prefix) {
try {
return await mkdtemp(path.join(os.tmpdir(), prefix));
} catch (error) {
exitOnError(EX_IOERR)(error);
exitWithCode(EX_IOERR, error);
}
}

Expand All @@ -230,7 +225,7 @@ export async function makeTempDirectory(prefix) {
export function setupEnvironment() {
setupWinstonLogger();
if (!local) {
exitOnError(EX_USAGE)("destination path $TO not set!");
exitWithCode(EX_USAGE, "destination path $TO not set!");
}
if (localAddress) {
https.globalAgent.options.localAddress = localAddress;
Expand Down