lib/nanika-storage.ts
/** 伺かに必要なストレージ操作を扱うディレクトリオブジェクト */
/** -- */ // doc comment が後にないとtypedocによってmoduleの情報が出力されないので
import {FileSystemObject} from "fso";
import {
UkagakaContainerChildType,
UkagakaContainerChildTypes,
UkagakaContainerStandaloneType,
UkagakaContainerType,
UkagakaDescriptInfo,
UkagakaInstallInfo,
} from "ukagaka-install-descript-info";
import {
HasNanikaContainerInfoDirectory,
NanikaBaseDirectory,
NanikaContainerDirectory,
NanikaContainerSyncDirectory,
NanikaContainerSyncEntry,
NanikaContainerSyncFile,
Profile,
} from "./nanika-container-directory";
export {
Profile,
NanikaContainerSyncEntry,
HasNanikaContainerInfoDirectory,
NanikaBaseDirectory,
NanikaContainerDirectory,
NanikaContainerSyncDirectory,
NanikaContainerSyncFile,
};
/** インストール結果 */
export interface NanikaStorageInstallResult {
/** インストール先のディレクトリ名 */
directory: string;
/** 種類 */
type: UkagakaContainerType;
}
/** 伺かベースウェアのルートディレクトリオブジェクト */
export class NanikaStorage extends NanikaBaseDirectory {
private static async _filterRemove(target: FileSystemObject, exceptPaths: string[]) {
let toRemoveChildren: FileSystemObject[];
try {
toRemoveChildren = await target.filteredChildrenAll(exceptPaths);
} catch (error) {
if (error.code === "ENOENT") return;
throw error;
}
for (const child of toRemoveChildren.reverse()) {
if (await child.isDirectory()) {
await (child as FileSystemObject).rmdir();
} else {
await (child as FileSystemObject).unlink();
}
}
}
private static async _mergeInstallDirectory( // sourceをFileSystemObjectに変換するための苦肉の策
source: HasNanikaContainerInfoDirectory | FileSystemObject, target: FileSystemObject, install: UkagakaInstallInfo,
) {
if (install.refresh) {
if (install.refreshundeletemask) {
await NanikaStorage._filterRemove(target, install.refreshundeletemask);
} else {
await target.rmAll();
}
}
const childSourceDirectories: string[] = [];
if (install.type === "ghost" || install.type === "shell") {
for (const type of UkagakaContainerChildTypes) {
const childDirectory = install.child(type).directory;
if (childDirectory) {
childSourceDirectories.push(install.child(type).source.directory || childDirectory);
}
}
}
await target.filteredMergeDirectory(source as FileSystemObject, childSourceDirectories);
}
/**
* @param paths ベースウェアのホームディレクトリ
*/
constructor(...paths: string[]) {
super(...paths);
}
/** profile のファイルパス */
profile() {
return this.new("profile/base.profile.json");
}
/**
* "/ghost" ディレクトリ
*/
ghostBase() { return this.new("ghost"); }
/**
* "/balloon" ディレクトリ
*/
balloonBase() { return this.new("balloon"); }
/**
* "/ghost/(ghostId)/shell" ディレクトリ
* @param dirpath ghost ディレクトリ名
*/
shellBase(dirpath: string) { return this.ghost(dirpath).new("shell"); }
/**
* "/plugin" ディレクトリ
*/
pluginBase() { return this.new("plugin"); }
/**
* "/headline" ディレクトリ
*/
headlineBase() { return this.new("headline"); }
/**
* "/calendar/skin" ディレクトリ
*/
calendarSkinBase() { return this.new("calendar/skin"); }
/**
* "/calendar/plugin" ディレクトリ
*/
calendarPluginBase() { return this.new("calendar/plugin"); }
/**
* "/ghost/(ghostId)" ディレクトリ
* @param dirpath ghost ディレクトリ名
*/
ghost(dirpath: string) { return new NanikaGhostDirectory(this.ghostBase().toString(), dirpath); }
/**
* "/balloon/(balloonId)" ディレクトリ
* @param dirpath balloon ディレクトリ名
*/
balloon(dirpath: string) { return new NanikaBalloonDirectory(this.balloonBase().toString(), dirpath); }
/**
* "/ghost/(ghostId)/ghost/master" ディレクトリ
* @param dirpath ghost ディレクトリ名
*/
ghostMaster(dirpath: string) {
return new NanikaGhostMasterDirectory(this.ghost(dirpath).toString(), "ghost/master");
}
/**
* "/ghost/(ghostId)/shell/(shellId)" ディレクトリ
* @param dirpath ghost ディレクトリ名
* @param shellpath shell ディレクトリ名
*/
shell(dirpath: string, shellpath: string) {
return new NanikaShellDirectory(this.ghost(dirpath).toString(), "shell", shellpath);
}
/**
* "/plugin/(pluginId)" ディレクトリ
* @param dirpath plugin ディレクトリ名
*/
plugin(dirpath: string) { return new NanikaPluginDirectory(this.pluginBase().toString(), dirpath); }
/**
* "/headline/(headlineId)" ディレクトリ
* @param dirpath headline ディレクトリ名
*/
headline(dirpath: string) { return new NanikaHeadlineDirectory(this.headlineBase().toString(), dirpath); }
/**
* "/calendar/skin/(calendarSkinId)" ディレクトリ
* @param dirpath calendar skin ディレクトリ名
*/
calendarSkin(dirpath: string) { return new NanikaCalendarSkinDirectory(this.calendarSkinBase().toString(), dirpath); }
/**
* "/calendar/plugin/(calendarPluginId)" ディレクトリ
* @param dirpath calendar plugin ディレクトリ名
*/
calendarPlugin(dirpath: string) {
return new NanikaCalendarPluginDirectory(this.calendarPluginBase().toString(), dirpath);
}
/**
* ghost ディレクトリ全部
*/
async ghosts() {
return (await this.ghostBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaGhostDirectory(child.toString()));
}
/**
* balloon ディレクトリ全部
*/
async balloons() {
return (await this.balloonBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaBalloonDirectory(child.toString()));
}
/**
* shell ディレクトリ全部
* @param dirpath ghost ディレクトリ名
*/
async shells(dirpath: string) {
return (await this.shellBase(dirpath).filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaShellDirectory(child.toString()));
}
/**
* plugin ディレクトリ全部
*/
async plugins() {
return (await this.pluginBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaPluginDirectory(child.toString()));
}
/**
* headline ディレクトリ全部
*/
async headlines() {
return (await this.headlineBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaHeadlineDirectory(child.toString()));
}
/**
* calendar skin ディレクトリ全部
*/
async calendarSkins() {
return (await this.calendarSkinBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaCalendarSkinDirectory(child.toString()));
}
/**
* calendar plugin ディレクトリ全部
*/
async calendarPlugins() {
return (await this.calendarPluginBase().filteredChildren(async (child) => await child.isDirectory()))
.map((child) => new NanikaCalendarPluginDirectory(child.toString()));
}
/** ゴースト名全部 */
async ghostNames() {
return Promise.all((await this.ghosts()).map((child) => child.master().name()));
}
/** バルーン名全部 */
async balloonNames() {
return Promise.all((await this.balloons()).map((child) => child.name()));
}
/**
* シェル名全部
* @param dirpath ghost ディレクトリ名
*/
async shellNames(dirpath: string) {
return Promise.all((await this.shells(dirpath)).map((child) => child.name()));
}
/** プラグイン名全部 */
async pluginNames() {
return Promise.all((await this.plugins()).map((child) => child.name()));
}
/** ヘッドライン名全部 */
async headlineNames() {
return Promise.all((await this.headlines()).map((child) => child.name()));
}
/** カレンダースキン名全部 */
async calendarSkinNames() {
return Promise.all((await this.calendarSkins()).map((child) => child.name()));
}
/** カレンダープラグイン名全部 */
async calendarPluginNames() {
return Promise.all((await this.calendarPlugins()).map((child) => child.name()));
}
/**
* ゴースト名
* @param dirpath ghost ディレクトリ名
*/
async ghostName(dirpath: string) {
return (await this.ghost(dirpath)).name();
}
/**
* バルーン名
* @param dirpath balloon ディレクトリ名
*/
async balloonName(dirpath: string) {
return (await this.balloon(dirpath)).name();
}
/**
* シェル名
* @param dirpath ghost ディレクトリ名
* @param shellpath shell ディレクトリ名
*/
async shellName(dirpath: string, shellpath: string) {
return (await this.shell(dirpath, shellpath)).name();
}
/**
* プラグイン名
* @param dirpath plugin ディレクトリ名
*/
async pluginName(dirpath: string) {
return (await this.plugin(dirpath)).name();
}
/**
* ヘッドライン名
* @param dirpath headline ディレクトリ名
*/
async headlineName(dirpath: string) {
return (await this.headline(dirpath)).name();
}
/**
* カレンダースキン名
* @param dirpath calendar skin ディレクトリ名
*/
async calendarSkinName(dirpath: string) {
return (await this.calendarSkin(dirpath)).name();
}
/**
* カレンダープラグイン名
* @param dirpath calendar plugin ディレクトリ名
*/
async calendarPluginName(dirpath: string) {
return (await this.calendarPlugin(dirpath)).name();
}
/**
* アンインストールします
* @param type 種類
* @param dirpath ゴーストのディレクトリ名
*/
async uninstall(type: "ghost", dirpath: string): Promise<void>;
/**
* アンインストールします
* @param type 種類
* @param dirpath バルーンのディレクトリ名
*/
async uninstall(type: "balloon", dirpath: string): Promise<void>; // tslint:disable-line unified-signatures
/**
* アンインストールします
* @param type 種類
* @param dirpath ゴーストのディレクトリ名
* @param shellpath シェルのディレクトリ名
*/
async uninstall(type: "shell", dirpath: string, shellpath: string): Promise<void>;
/**
* アンインストールします
* @param type 種類
* @param dirpath プラグインのディレクトリ名
*/
async uninstall(type: "plugin", dirpath: string): Promise<void>; // tslint:disable-line unified-signatures
/**
* アンインストールします
* @param type 種類
* @param dirpath ヘッドラインのディレクトリ名
*/
async uninstall(type: "headline", dirpath: string): Promise<void>; // tslint:disable-line unified-signatures
/**
* アンインストールします
* @param type 種類
* @param dirpath カレンダースキンのディレクトリ名
*/
async uninstall(type: "calendar.skin", dirpath: string): Promise<void>; // tslint:disable-line unified-signatures
/**
* アンインストールします
* @param type 種類
* @param dirpath カレンダープラグインのディレクトリ名
*/
async uninstall(type: "calendar.plugin", dirpath: string): Promise<void>; // tslint:disable-line unified-signatures
async uninstall(type: UkagakaContainerStandaloneType, dirpath: string, shellpath?: string) {
switch (type) {
case "ghost": await this.ghost(dirpath).rmAll(); break;
case "balloon": await this.balloon(dirpath).rmAll(); break;
case "shell": await this.shell(dirpath, shellpath as string).rmAll(); break;
case "plugin": await this.plugin(dirpath).rmAll(); break;
case "headline": await this.headline(dirpath).rmAll(); break;
case "calendar.skin": await this.calendarSkin(dirpath).rmAll(); break;
case "calendar.plugin": await this.calendarPlugin(dirpath).rmAll(); break;
}
}
/**
* インストールします
* @param nar インストール元
* @param dirpath ゴーストのディレクトリ名
* @param sakuraname インストールを受け付けたゴーストのさくら側名
*/
async install(nar: HasNanikaContainerInfoDirectory, dirpath: string | undefined, sakuraname: string | undefined) {
const install = await nar.installInfo();
if (install.accept != null && install.accept !== sakuraname) return [];
switch (install.type) {
case "ghost":
return await this._installGhost(nar, install);
case "shell":
if (!dirpath) throw new Error("dirpath required");
return await this._installShell(nar, install, dirpath);
case "balloon":
return await this._installBalloon(nar, install);
case "plugin":
return await this._installPlugin(nar, install);
case "headline":
return await this._installHeadline(nar, install);
case "calendar.skin":
return await this._installCalendarSkin(nar, install);
case "calendar.plugin":
return await this._installCalendarPlugin(nar, install);
case "supplement":
return await this._installSupplement(nar, install);
case "package":
return await this._installPackage(nar, dirpath, sakuraname);
default:
throw new Error("unknown type");
}
}
private async _installChild(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
switch (install.type as UkagakaContainerChildType) {
case "balloon":
return await this._installBalloon(nar, install);
case "plugin":
return await this._installPlugin(nar, install);
case "headline":
return await this._installHeadline(nar, install);
case "calendar.skin":
return await this._installCalendarSkin(nar, install);
case "calendar.plugin":
return await this._installCalendarPlugin(nar, install);
default:
throw new Error("unknown type");
}
}
private async _installGhost(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
if (!install.directory) throw new Error("install.txt directory entry required");
const targetDirectory = install.directory;
const target = this.ghost(targetDirectory);
await NanikaStorage._mergeInstallDirectory(nar, target, install);
const childInstallResults = await this._installChildren(nar, install);
return [{directory: targetDirectory, type: "ghost"}].concat(childInstallResults) as NanikaStorageInstallResult[];
}
private async _installShell(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo, dirpath: string) {
if (!install.directory) throw new Error("install.txt directory entry required");
const targetDirectory = install.directory;
const target = this.shell(dirpath, targetDirectory);
await NanikaStorage._mergeInstallDirectory(nar, target, install);
const childInstallResults = await this._installChildren(nar, install);
return [{directory: targetDirectory, type: "shell"}].concat(childInstallResults) as NanikaStorageInstallResult[];
}
private async _installBalloon(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "balloon", this.balloon(install.directory));
}
private async _installPlugin(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "plugin", this.plugin(install.directory));
}
private async _installHeadline(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "headline", this.headline(install.directory));
}
private async _installCalendarSkin(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "calendar.skin", this.calendarSkin(install.directory));
}
private async _installCalendarPlugin(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "calendar.plugin", this.calendarPlugin(install.directory));
}
private async _installSupplement(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
return await this._installSingleContainer(nar, install, "supplement", this.ghost(install.directory));
}
private async _installSingleContainer(
nar: HasNanikaContainerInfoDirectory,
install: UkagakaInstallInfo,
type: UkagakaContainerChildType | "supplement",
target: FileSystemObject,
) {
if (!install.directory) throw new Error("install.txt directory entry required");
const targetDirectory = install.directory;
await NanikaStorage._mergeInstallDirectory(nar, target, install);
return [{directory: targetDirectory, type}] as NanikaStorageInstallResult[];
}
private async _installPackage(
nar: HasNanikaContainerInfoDirectory,
dirpath: string | undefined,
sakuraname: string | undefined,
) {
let installResults: NanikaStorageInstallResult[] = [];
for (const child of await nar.children()) {
if (child.isDirectory()) {
installResults = installResults.concat(
await this.install(child as HasNanikaContainerInfoDirectory, dirpath, sakuraname),
);
}
}
return installResults;
}
private async _installChildren(nar: HasNanikaContainerInfoDirectory, install: UkagakaInstallInfo) {
let installResults: NanikaStorageInstallResult[] = [];
for (const type of UkagakaContainerChildTypes) {
const childDirectory = install.child(type).directory;
// *.directory,dir がある場合は同時に別の種類の物をインストールする
if (childDirectory) {
// 同時インストール物のソースディレクトリが展開後ディレクトリと別名の場合
// *.source.directory,dirを使う
// そうでなければソースディレクトリは展開後ディレクトリ*.directory,dirと同名として扱う
const childSourceDirectory = install.child(type).source.directory || childDirectory;
const childNar = nar.new(childSourceDirectory) as HasNanikaContainerInfoDirectory;
const childInstall = new UkagakaInstallInfo();
childInstall.type = type;
childInstall.directory = childDirectory;
childInstall.refresh = install.child(type).refresh;
childInstall.refreshundeletemask = install.child(type).refreshundeletemask;
const childInstallResults = await this._installChild(childNar, childInstall);
installResults = installResults.concat(childInstallResults);
}
}
return installResults;
}
}
export class NanikaGhostDirectory extends NanikaContainerDirectory {
/** "/ghost/master" ディレクトリ */
master() {
return new NanikaGhostMasterDirectory(this.toString(), "ghost/master");
}
/** profile のファイルパス */
profile() {
return this.master().profile();
}
/** "descript.txt" の内容 */
descriptInfo(): Promise<UkagakaDescriptInfo.Ghost> { // UkagakaDescriptInfoをimportしないとエラーかつ書かないとエラーなので
return super.descriptInfoByType("ghost");
}
}
export class NanikaGhostMasterDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/ghost.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("ghost");
}
}
export class NanikaBalloonDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/balloon.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("balloon");
}
}
export class NanikaShellDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/shell.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("shell");
}
}
export class NanikaPluginDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/plugin.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("plugin");
}
}
export class NanikaHeadlineDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/headline.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("headline");
}
}
export class NanikaCalendarSkinDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/calendar.skin.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("calendar.skin");
}
}
export class NanikaCalendarPluginDirectory extends NanikaContainerDirectory {
/** profile のファイルパス */
profile() {
return this.new("profile/calendar.plugin.profile.json");
}
/** "descript.txt" の内容 */
descriptInfo() {
return super.descriptInfoByType("calendar.plugin");
}
}