rename sites to manager

This commit is contained in:
lightling 2024-11-06 17:14:31 -05:00
parent 5e866ca8d2
commit ed025533cd
Signed by: lightling
GPG key ID: F1F29650D537C773
11 changed files with 15 additions and 15 deletions

View file

@ -0,0 +1,14 @@
{
"name": "manager",
"private": true,
"version": "0.0.0",
"type": "module",
"devDependencies": {
"chalk": "5.3.0",
"symlink-dir": "6.0.0"
},
"scripts": {
"cms-to-static": "node scripts/cms-to-static.js",
"set-current": "node scripts/set-current.js"
}
}

View file

@ -0,0 +1,191 @@
import chalk from 'chalk';
import readlinePromises from 'readline/promises';
import { Readable } from 'stream';
import {
existsSync,
createWriteStream,
} from 'fs';
import { finished } from 'stream/promises';
import {
mkdir,
unlink,
writeFile,
} from 'fs/promises';
import { log } from '../src/log.js';
/**
* Validates a string as a MediaEntry id
* @param {string} str gets a valid id for a MediaEntry; it cannot have symbols that are not valid in paths
* @returns {string} the validated string
*/
const getValidId = (str) => (str || '').replace(/( |\/|\\)/gm,'_');
/**
* Saves the given url to the given destination
* @param {string} url the url of the file to save
* @param {string} destination the file destination
*/
const saveFileFromUrl = async (url, destination) => {
if (existsSync(destination)) {
log(`Deleting existing file ${chalk.italic(destination)}`, 'debug');
await unlink(destination);
log(`Deleted file ${chalk.italic(destination)}`, 'info');
}
log(`Downloading file ${url}`, 'debug');
const res = await fetch(url);
log(`Saving file ${chalk.italic(url)} to ${chalk.italic(destination)}`, 'debug');
const fileStream = createWriteStream(destination, { flags: 'wx' });
await finished(Readable.fromWeb(res.body).pipe(fileStream));
log(`File ${chalk.italic(url)} saved to ${chalk.italic(destination)}`, 'info');
};
/**
* Saves a media list's entries to the given destination and returns an updated list where the Strapi urls have been replaced
* @param {MediaList} inVal the media list to save files from
* @param {string} fetchUrl the Strapi API fetch url
* @param {string} destination the parent destination where the media list is saving files/folders in
* @param {string} destinationShort the shortened destination, where 'sites/{my-site}/' has been removed so that the file can be '/content/{file/path}'
* @returns {Promise<MediaList>} the updated media list
*/
const saveVariantsToDestination = async (inVal, fetchUrl, destination, destinationShort) => {
const rootUrl = fetchUrl.replace(/((http)|(https)):\/\//, '').split('/')[0];
const outVal = JSON.parse(JSON.stringify(inVal));
// Loop over each entry in the media list
await Promise.all(Object.keys(outVal.entries).map(async entryId => {
const entryPath = `${destination}/${entryId}`;
if (!existsSync(entryPath)) {
log(`Creating directory at ${chalk.italic(entryPath)}`, 'debug');
await mkdir(entryPath);
log(`Created directory ${chalk.italic(entryPath)}`, 'info');
}
// Loop over each variant in the media entry
await Promise.all(outVal.entries[entryId].variants.map(async (variant, index) => {
// save the main file and update in the variant info
const filePath = `${entryPath}/${variant.url.replace('/uploads/', '')}`;
saveFileFromUrl(`http://${rootUrl}${variant.url}`, filePath);
outVal.entries[entryId].variants[index].url = `/content/${destinationShort}/${entryId}/${variant.url.replace('/uploads/', '')}`;
// save the thumbnail and update in the variant info
const thumbnailPath = `${entryPath}/${variant.thumbnailUrl.replace('/uploads/', '')}`;
saveFileFromUrl(`http://${rootUrl}${variant.thumbnailUrl}`, thumbnailPath);
outVal.entries[entryId].variants[index].thumbnailUrl = `/content/${destinationShort}/${entryId}/${variant.thumbnailUrl.replace('/uploads/', '')}`;
if (true) {
await writeFile(`${entryPath}/info.json`, JSON.stringify(outVal.entries[entryId], null, 2));
outVal.entries[entryId] = `/content/${destinationShort}/${entryId}/info.json`;
}
}));
}));
return new Promise(resolve => resolve(outVal));
};
/**
* Maps Strapi API response to a mackenzii media list
* @param {StrapiMediaList} inVal the Strapi API response
* @returns {Promise<MediaList>} the media list mapped from the Strapi API
*/
const mapStrapiResponseToMackenzii = async (inVal) => {
const { data } = inVal;
const outVal = {};
// handle list basic fields
outVal.title = data.title;
// handle list entries
outVal.entries = {};
data.entries.forEach(entry => {
// give it an id
const id = `${getValidId(entry.date)}_${getValidId(entry.title)}_${getValidId(entry.documentId)}`;
// copy all values at first...
outVal.entries[id] = {...entry};
// ...then delete strapi fields (as there are more to copy than delete)
delete outVal.entries[id].id;
delete outVal.entries[id].documentId;
delete outVal.entries[id].createdAt;
delete outVal.entries[id].updatedAt;
delete outVal.entries[id].publishedAt;
delete outVal.entries[id].locale;
// ...then handle variants
outVal.entries[id].variants = entry.variants.map(variant => ({
alternativeText: variant.alternativeText,
caption: variant.caption,
// the images will currently be mapped to strapi, to be handled with file grabber
url: variant.url,
thumbnailUrl: variant.formats['thumbnail'].url, // TBD: non-image assets
}));
// ...also post-process tags into an array
outVal.entries[id].tags = (outVal.entries[id].tags || '').split(/,| |;/).filter(val => val !== '');
});
return new Promise(resolve => resolve(outVal));
};
(async () => {
console.log('NOTE: This is currently only built to support gallery lists. Article lists and other content are not supported');
const rl = readlinePromises.createInterface(process.stdin, process.stdout);
let fetchUrl = '';
try {
fetchUrl = await rl.question('Enter the URL to fetch from: ');
} catch (err) {
log('There was an error: ', 'error', 'cms-to-static::main::Q1', err);
} finally {
if (!fetchUrl) {
log('An invalid URL was given. Quitting...', 'log', 'cms-to-static::main::Q1');
rl.close();
return;
} else if (!fetchUrl.includes('?')) {
fetchUrl += '?populate[0]=entries&populate[1]=entries.variants&populate[2]=tags&populate[3]=tags.value'
}
}
let destination = '';
try {
destination = await rl.question('Enter the directory to save the response\n(note: this should be a path relative to "sites";\n{your/input} will become sites/{your/input}):');
destination = 'sites/' + destination;
} catch (err) {
log('There was an error: ', 'error', 'cms-to-static::main::Q2', err);
} finally {
rl.close();
if (!destination) {
log('An invalid path was given. Quitting...', 'log', 'cms-to-static::main::Q2');
return;
} else {
if (!existsSync(destination)) {
await mkdir(destination, { recursive: true });
}
}
}
let res = {};
try {
res = await (await fetch(fetchUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})).json();
} catch (err) {
log('There was an error: ', 'error', 'cms-to-static::main', err);
}
log('Writing...', 'log', 'cms-to-static::main::writing');
try {
await writeFile(`${destination}/in.json`, JSON.stringify(res, null, 2));
mapStrapiResponseToMackenzii(res)
.then(list => saveVariantsToDestination(list, fetchUrl, destination, destination.replace('sites/', '').split('/').slice(1).join('/')))
.then(list => writeFile(`${destination}/out.json`, JSON.stringify(list, null, 2)));
log('Done!', 'log', 'cms-to-static::main::writing');
} catch (err) {
log('There was an error: ', 'error', 'cms-to-static::main::writing', err);
}
})();

View file

@ -0,0 +1,3 @@
import { setCurrent } from '../src/set-current.js';
setCurrent(process.env.npm_config_site);

103
projects/manager/src/log.js Normal file
View file

@ -0,0 +1,103 @@
import chalk from 'chalk';
/**
* Formats the given date for logging
* @param {Date} date the date to format
* @returns {string} the formatted date string in yyyy/mm/dd hh/mm/ss/SSS
*/
export const time = (date) => {
/**
* Stringifies the given number, prepends with 0s if the number string isn't long enough
* @param {number} num the number to stringify
* @param {number} expectedLength how long the number should be
* @returns {string} the stringified number
*/
const stringifyNumber = (num, expectedLength) => {
let str = `${num}`;
while (str.length < expectedLength) {
str = `0${str}`;
}
return str;
};
const year = date.getFullYear()
const month = stringifyNumber(date.getMonth(), 2);
const day = stringifyNumber(date.getDay(), 2);
const hour = stringifyNumber(date.getHours(), 2);
const minute = stringifyNumber(date.getMinutes(), 2);
const second = stringifyNumber(date.getSeconds(), 2);
const millisecond = stringifyNumber(date.getMilliseconds(), 2);
return `${year}/${month}/${day} ${hour}:${minute}:${second}.${millisecond}`;
}
/**
* Logs a message to the console
* @param {string} message the message to log
* @param {'debug'|'info'|'log'|'warn'|'error'} kind the kind of message being logged; denotes logging level
* - debug is lowest level of message (0)
* - info is the second lowest level of message (1)
* - log is the default level level of message (2)
* - warn is the second highest level of message (3)
* - error is the highest level of message (4)
* @param {string} context the context; if undefined, assume part of a parent context and indent the message
* @param {string} extraData any extra data (e.g. if there was an error, pass the error object here)
*/
export const log = (message, kind, context, extraData) => {
const logLevel = process.env.npm_config_logging || 2;
const formatted = `${chalk.bold(`[${time(new Date())}] `)}${context ? context + ': ' : ' '}${message}`;
switch (kind) {
case 'debug': {
if (logLevel > 0) {
return;
}
if (extraData) {
console.debug(`${chalk.gray.bold('[debug]')}${formatted}`, extraData);
} else {
console.debug(`${chalk.gray.bold('[debug]')}${formatted}`);
}
break;
}
case 'info': {
if (logLevel > 1) {
return;
}
if (extraData) {
console.info(`${chalk.white.bold('[info]')}${formatted}`, extraData);
} else {
console.info(`${chalk.white.bold('[info]')}${formatted}`);
}
break;
}
case 'warn': {
if (logLevel > 3) {
return;
}
if (extraData) {
console.warn(`${chalk.yellow.bold('[warn]')}${formatted}`, extraData);
} else {
console.warn(`${chalk.yellow.bold('[warn]')}${formatted}`);
}
break;
}
case 'error': {
if (extraData) {
console.error(`${chalk.red.bold('[error]')}${formatted}`, extraData);
} else {
console.error(`${chalk.red.bold('[error]')}${formatted}`);
}
break;
}
default: {
if (logLevel > 2) {
return;
}
if (extraData) {
console.log(`${chalk.blue.bold('[log]')}${formatted}`, extraData);
} else {
console.log(`${chalk.blue.bold('[log]')}${formatted}`);
}
break;
}
}
};

View file

@ -0,0 +1,24 @@
import symlinkDir from 'symlink-dir';
import { existsSync } from 'fs';
import { mkdir, unlink, writeFile } from 'fs/promises';
export const setCurrent = async (site, ctx) => {
const managerPath = ctx?.managerPath || '../manager';
const frontendPath = ctx?.frontendPath || '../frontend';
if (!!site) {
await symlinkDir(`${managerPath}/sites/${site}`, `${frontendPath}/content`);
if (!existsSync(`${frontendPath}/dist`)) {
await mkdir(`${frontendPath}/dist`);
}
await symlinkDir(`${managerPath}/sites/${site}`, `${frontendPath}/dist/content`);
if (existsSync(`${managerPath}/sites/current.txt`)) {
await unlink(`${managerPath}/sites/current.txt`);
}
await writeFile(`${managerPath}/sites/current.txt`, site, { encoding: 'utf-8' });
console.log('done');
} else {
console.error('Parameter "site" was not provided!');
}
};