Skip to content

Commit

Permalink
Implement migration from v0.1.0 -> v0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
MaddyGuthridge committed Aug 28, 2024
1 parent 5e7e137 commit 1033bb4
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 7 deletions.
2 changes: 1 addition & 1 deletion src/lib/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export function hashAndSalt(salt: string, password: string): string {
* This is responsible for generating and storing a secure password, thereby
* creating the default "admin" account.
*/
export async function authSetup(cookies: Cookies): Promise<FirstRunCredentials> {
export async function authSetup(cookies?: Cookies): Promise<FirstRunCredentials> {
const username = 'admin';

// generate password using 4 random dictionary words
Expand Down
13 changes: 13 additions & 0 deletions src/lib/server/data/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ export const ConfigJsonStruct = object({
/** Main configuration for the portfolio, `config.json` */
export type ConfigJson = Infer<typeof ConfigJsonStruct>;

/**
* Return the version of the configuration. Used to determine if a migration is
* necessary.
*
* This does not validate any of the data.
*/
export async function getConfigVersion(): Promise<string> {
const data = JSON.parse(await readFile(CONFIG_JSON, { encoding: 'utf-8' }));

// Note: v0.1.0 did not have a version number in the file
return data.version ?? '0.1.0';
}

/** Return the configuration, stored in `/data/config.json` */
export async function getConfig(): Promise<ConfigJson> {
const data = await readFile(CONFIG_JSON, { encoding: 'utf-8' });
Expand Down
11 changes: 10 additions & 1 deletion src/lib/server/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
* of effort for very little gain.
*/

import { getConfig, type ConfigJson } from './config';
import { version } from '$app/environment';
import { getConfig, getConfigVersion, type ConfigJson } from './config';
import { getGroupData, listGroups, type GroupData } from './group';
import { getItemData, listItems, type ItemData } from './item';
import { invalidateLocalConfigCache } from './localConfig';
import migrate from './migrations';
import { getReadme } from './readme';

/** Public global data for the portfolio */
Expand All @@ -30,6 +32,13 @@ export type PortfolioGlobals = {
* references are valid).
*/
async function loadPortfolioGlobals(): Promise<PortfolioGlobals> {
// Check if a migration is needed
const dataVersion = await getConfigVersion();

if (dataVersion !== version) {
await migrate(dataVersion);
}

const config = await getConfig();
const readme = await getReadme();

Expand Down
16 changes: 11 additions & 5 deletions src/lib/server/data/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const LinksArray = array(
object({
groupId: string(),
style: LinkStyleStruct,
title: string(),
}),
array(string()),
])
Expand All @@ -71,17 +72,17 @@ export const ItemInfoFullStruct = intersection([
/** URLs associated with the label */
urls: object({
/** URL of the source repository of the label */
repo: optional(RepoInfoStruct),
repo: nullable(RepoInfoStruct),

/** URL of the site demonstrating the label */
site: optional(string()),
site: nullable(string()),

/** URL of the documentation site for the label */
docs: optional(string()),
docs: nullable(string()),
}),

/** Information about the package distribution of the label */
package: optional(PackageInfoStruct),
package: nullable(PackageInfoStruct),
}),
]);

Expand Down Expand Up @@ -173,7 +174,12 @@ export async function createItem(groupId: string, itemId: string, name: string,
// TODO: Generate a random color for the new item
color: '#aa00aa',
links: [],
urls: {},
urls: {
repo: null,
site: null,
docs: null
},
package: null,
icon: null,
banner: null,
});
Expand Down
19 changes: 19 additions & 0 deletions src/lib/server/data/migrations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getDataDir } from '../dataDir';
import migrateFromV010 from './v0.1.0';

/** Perform a migration from the given version */
export default async function migrate(oldVersion: string) {
console.log(`Data directory uses version ${oldVersion}. Migration needed`);
try {
if (oldVersion === '0.1.0') {
await migrateFromV010(getDataDir());
} else {
console.log(`Migrate: unrecognised old version ${oldVersion}`);
process.exit(1);
}
} catch (e) {
console.log('!!! Error during migration');
console.log(e);
process.exit(1);
}
}
146 changes: 146 additions & 0 deletions src/lib/server/data/migrations/v0.1.0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Migration from v0.1.0 to v0.2.0
*
* Since this will likely only be used by me, it probably won't be all that
* reliable.
*
* # Instructions
*
* 1. Ensure your data is backed up to a git repo
* 2. Remove the data directory to reset the server
* 3. Set up the server using the git repo URL
* 4. Note the new auth information
*
* # Incompatible data
*
* Some data is lost in this migration (mainly because I couldn't be bothered
* to implement anything more than the minimum).
*
* * `README.md` clobbered by `info.md`.
* * Sorting and listing of items within groups. All items are listed.
* * Ordering of links (formerly associations) in items. Old data format did
* not order them.
* * Link style (can't be bothered to look up data in old format).
* * Filter groups per group.
*/

import fs from 'fs/promises';
import { setConfig, type ConfigJson } from '../config';
import { listGroups, setGroupInfo } from '../group';
import type { RepoInfo } from '../itemRepo';
import type { PackageInfo } from '../itemPackage';
import { LinksArray, listItems, setItemInfo } from '../item';
import type { Infer } from 'superstruct';
import { version } from '$app/environment';

export default async function migrate(dataDir: string) {
console.log(`Begin data migration v0.1.0 -> ${version}`);
await config(dataDir);
await readme(dataDir);

// For each group
for (const groupId of await listGroups()) {
// Migrate each item
for (const itemId of await listItems(groupId)) {
await itemInfo(dataDir, groupId, itemId);
}
// Then migrate the group info
await groupInfo(dataDir, groupId);
}
console.log('Data migration complete');
}

/** Migrate config.json */
async function config(dataDir: string) {
console.log(' config.json');
const configJsonPath = `${dataDir}/config.json`;
const oldConfig: { name: string } = JSON.parse(await fs.readFile(configJsonPath, { encoding: 'utf-8' }));

const groupsList = await listGroups();

const newConfig: ConfigJson = {
siteName: oldConfig.name,
// Assumes that all groups are visible, since there is no visibility
// setting for groups in 0.1.0
listedGroups: groupsList,
color: '#ffaaff',
version: '0.2.0',
};

await setConfig(newConfig);
}

/** Migrate info.md -> README.md */
async function readme(dataDir: string) {
console.log(' info.md -> README.md');
await fs.unlink(`${dataDir}/README.md`);
await fs.rename(`${dataDir}/info.md`, `${dataDir}/README.md`);
}

async function itemInfo(dataDir: string, groupId: string, itemId: string) {
console.log(` Item: ${groupId}/${itemId}`);

const itemPath = `${dataDir}/${groupId}/${itemId}`;
const item: {
name: string,
description: string,
color: string,
/** External links (not internal associative links) */
links?: {
repo?: RepoInfo,
site?: string,
docs?: string,
},
/** Internal (associative) links */
associations: Record<string, string[]>,
icon?: string,
banner?: string,
package?: PackageInfo,
} = JSON.parse(await fs.readFile(`${itemPath}/info.json`, { encoding: 'utf-8' }));

const links: Infer<typeof LinksArray> = [];

if (item.links) {
for (const linkedGroup of Object.keys(item.associations)) {
links.push([{ groupId: linkedGroup, title: linkedGroup, style: 'chip' }, item.associations[linkedGroup]]);
}
}

await setItemInfo(groupId, itemId, {
name: item.name,
description: item.description,
color: item.color,
icon: item.icon ?? null,
banner: item.banner ?? null,
links,
urls: {
repo: item.links?.repo ?? null,
site: item.links?.site ?? null,
docs: item.links?.docs ?? null,
},
package: item.package ?? null,
});
}

async function groupInfo(dataDir: string, groupId: string) {
console.log(` Group: ${groupId}`);

const groupPath = `${dataDir}/${groupId}`;
const group: {
name: string,
description: string,
color: string,
} = JSON.parse(await fs.readFile(`${groupPath}/info.json`, { encoding: 'utf-8' }));

await setGroupInfo(groupId, {
name: group.name,
description: group.description,
color: group.color,
icon: null,
banner: null,
// Filter using all groups
filterGroups: await listGroups(),
// List all items
listedItems: await listItems(groupId),
});
}

0 comments on commit 1033bb4

Please sign in to comment.