lib/nanika-container-directory.ts
/** 伺かのコンテナを扱うディレクトリオブジェクト */
/** -- */ // doc comment が後にないとtypedocによってmoduleの情報が出力されないので
import * as fs from "fs";
import {FileSystemObject} from "fso";
import * as path from "path";
import {UkagakaContainerStandaloneType, UkagakaDescriptInfo, UkagakaInstallInfo} from "ukagaka-install-descript-info";
/** the profile */
export interface Profile {
[key: string]: any;
}
/** コンテナ(ghost, balloon等)の情報を取得できるディレクトリオブジェクト */
export interface HasNanikaContainerInfoDirectory {
toString(): string;
["new"](...paths: string[]): FileSystemObject | NanikaContainerSyncDirectory | NanikaContainerSyncFile;
children(): Promise<Array<FileSystemObject | NanikaContainerSyncDirectory | NanikaContainerSyncFile>>;
childrenAll(): Promise<Array<FileSystemObject | NanikaContainerSyncDirectory | NanikaContainerSyncFile>>;
/** "install.txt" の内容 */
installInfo(): Promise<UkagakaInstallInfo>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "ghost"): Promise<UkagakaDescriptInfo.Ghost>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "shell"): Promise<UkagakaDescriptInfo.Shell>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "balloon"): Promise<UkagakaDescriptInfo.Balloon>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "plugin"): Promise<UkagakaDescriptInfo.Plugin>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "headline"): Promise<UkagakaDescriptInfo.Headline>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "calendar.skin"): Promise<UkagakaDescriptInfo.CalendarSkin>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
descriptInfoByType(type: "calendar.plugin"): Promise<UkagakaDescriptInfo.CalendarPlugin>;
/**
* "descript.txt" の内容
*/
descriptInfo(): Promise<UkagakaDescriptInfo>;
relative(to: string | FileSystemObject): FileSystemObject;
/**
* 全ての子要素のstatsと内容をキャッシュした -Sync API を提供する新しいオブジェクトを生成する。
* make new directory object which provides -Sync APIs by cache all children's stats and contents.
*/
toCached(): Promise<NanikaContainerSyncDirectory>;
}
/** profile を読み込めるディレクトリオブジェクトのベースクラス */
export abstract class NanikaBaseDirectory extends FileSystemObject {
/** profile のファイルパス */
abstract profile(): FileSystemObject;
/**
* profile を書き込む
* @param profile profile
*/
async writeProfile(profile: Profile) {
const target = this.profile();
await target.parent().mkdirAll();
await target.writeFile(JSON.stringify(profile), {encoding: "utf8"});
}
/**
* profile を読み込む
*/
async readProfile() {
let data: string;
try {
data = await this.profile().readFile({encoding: "utf8"});
} catch (error) {
return {};
}
return data.length ? JSON.parse(data) as Profile : {} as Profile;
}
/**
* 全ての子要素のstatsと内容をキャッシュした -Sync API を提供する新しいオブジェクトを生成する。
* make new directory object which provides -Sync APIs by cache all children's stats and contents.
*/
async toCached() {
const children = await this.childrenAll();
const contents =
await Promise.all(children.map(async (child) =>
(await child.isDirectory()) ? undefined : child.readFile(),
));
const stats = await Promise.all(children.map((child) => child.stat()));
return new NanikaContainerSyncDirectory(
this.path,
children.map((child, index) =>
new NanikaContainerSyncFile(child.path, contents[index], stats[index]),
),
);
}
}
/**
* コンテナ(ghost, balloon等)のルートディレクトリオブジェクト
*
* some container(like ghost, balloon or some)'s root directory object
*/
export abstract class NanikaContainerDirectory extends NanikaBaseDirectory implements HasNanikaContainerInfoDirectory {
/** "install.txt" のファイルパス */
installTxt() { return this.new("install.txt"); }
/** "descript.txt" のファイルパス */
descriptTxt() { return this.new("descript.txt"); }
/** "install.txt" の内容 */
async installInfo() {
return UkagakaInstallInfo.parse(
await this.installTxt().readFile(),
);
}
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "ghost"): Promise<UkagakaDescriptInfo.Ghost>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "balloon"): Promise<UkagakaDescriptInfo.Balloon>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "shell"): Promise<UkagakaDescriptInfo.Shell>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "plugin"): Promise<UkagakaDescriptInfo.Plugin>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "headline"): Promise<UkagakaDescriptInfo.Headline>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "calendar.skin"): Promise<UkagakaDescriptInfo.CalendarSkin>;
/**
* "descript.txt" の内容
* @param type コンテナの種類
*/
async descriptInfoByType(type: "calendar.plugin"): Promise<UkagakaDescriptInfo.CalendarPlugin>;
async descriptInfoByType(type: UkagakaContainerStandaloneType) {
return UkagakaDescriptInfo.parse(
await this.descriptTxt().readFile(),
type as "ghost", // TODO: Narazaka: 他に方法あるかな?
) as UkagakaDescriptInfo;
}
/** "descript.txt" の内容 */
async descriptInfo() {
return UkagakaDescriptInfo.parse(
await this.descriptTxt().readFile(),
);
}
/** コンテナ名 */
async name() {
return (await this.descriptInfo()).name;
}
}
const childRe = new RegExp(`^\\.\\.(?:\\${path.sep}\\.\\.)*$`);
/** directory object which provides -Sync APIs */
export abstract class NanikaContainerSyncEntry {
static format(pathObject: path.ParsedPath) {
return new FileSystemObject(path.format(pathObject));
}
path: string;
constructor(path: string) { // tslint:disable-line no-shadowed-variable
this.path = path;
}
toString() {
return this.path;
}
get delimiter() {
return path.delimiter;
}
get sep() {
return path.sep;
}
parse() {
return path.parse(this.path);
}
normalize() {
return this;
}
basename() {
return new FileSystemObject(path.basename(this.path));
}
dirname() {
return new FileSystemObject(path.dirname(this.path));
}
extname() {
return path.extname(this.path);
}
isAbsolute() {
return path.isAbsolute(this.path);
}
relative(to: string | FileSystemObject) {
return new FileSystemObject(path.relative(this.path, to.toString()));
}
resolve(...paths: Array<string | FileSystemObject>) {
return new FileSystemObject(path.resolve(...paths.map((_path) => _path.toString()).concat([this.path])));
}
isChildOf(to: string | FileSystemObject) {
return childRe.test(path.relative(this.path, to.toString()));
}
}
/**
* 全ての子要素のstatsと内容をキャッシュした -Sync API を提供するコンテナ(ghost, balloon等)のルートディレクトリオブジェクト。
*
* container root directory object which provides -Sync APIs by cache all children's stats and contents.
*/
export class NanikaContainerSyncDirectory extends NanikaContainerSyncEntry implements HasNanikaContainerInfoDirectory {
private _childrenCache: Array<NanikaContainerSyncDirectory | NanikaContainerSyncFile>;
private _childrenAllCache: NanikaContainerSyncFile[];
private _indexes: {[path: string]: number} = {};
/**
* @param path パス
* @param childrenAllCache 子要素全てのstatsと内容のキャッシュ
*/
constructor(path: string, childrenAllCache: NanikaContainerSyncFile[]) { // tslint:disable-line no-shadowed-variable
super(path);
this._childrenAllCache = childrenAllCache;
this._makeIndexes();
}
installTxt() {
return this.new("install.txt");
}
descriptTxt() {
return this.new("descript.txt");
}
async isDirectory() { return true; }
isDirectorySync() { return true; }
async isFile() { return false; }
isFileSync() { return false; }
async exists() { return true; }
existsSync() { return true; }
new(...paths: string[]): NanikaContainerSyncDirectory | NanikaContainerSyncFile {
const newPath = path.join(this.path, ...paths);
const index = this._indexes[newPath];
if (index == null) {
return new NanikaContainerSyncFile(newPath);
} else {
const child = this._childrenAllCache[index];
if (child.isDirectorySync()) {
const childChildren = this._childrenAllCache
.filter((childCache) => childCache.isChildOf(child.toString()));
return new NanikaContainerSyncDirectory(child.path, childChildren);
} else {
return child;
}
}
}
childrenSync() {
if (this._childrenCache) return this._childrenCache;
const re = new RegExp(`^${this.path}[/\\\\][^/\\\\]+$`);
this._childrenCache = this._childrenAllCache
.filter((child) => re.test(child.toString()))
.map((child) => child.isDirectorySync() ? this.new(child.basename().toString()) : child);
return this._childrenCache;
}
async children() {
return this.childrenSync();
}
childrenAllSync() {
return this._childrenAllCache;
}
async childrenAll() {
return this.childrenAllSync();
}
async filteredChildrenAll(excepts: string[] | ((child: NanikaContainerSyncFile) => boolean)) {
return this.filteredChildrenAllSync(excepts);
}
filteredChildrenAllSync(excepts: string[] | ((child: NanikaContainerSyncFile) => boolean)) {
if (!excepts || !excepts.length) return this.childrenAllSync();
if (excepts instanceof Array) {
const exceptTargets = excepts.map((exceptPath) => path.join(this.path, exceptPath));
return (this.childrenAllSync())
.filter(
// 子パスは全ての除外パスと(無関係か除外パスの親)=!(子パスはいずれかの除外パス(そのものか除外パスの子))
// child path is (unrelated to || parent of) all of the except paths
// = !(child path is (same as || child of) at least one of the except paths)
(child) => !exceptTargets.find(
(exceptTarget) => exceptTarget === child.path || child.isChildOf(exceptTarget),
),
);
} else {
return (this.childrenAllSync()).filter(excepts);
}
}
async toCached() {
return this;
}
async installInfo() {
return this.installInfoSync();
}
installInfoSync() {
return UkagakaInstallInfo.parse(
(this.installTxt() as NanikaContainerSyncFile).readFileSync(),
);
}
async descriptInfoByType(type: "ghost"): Promise<UkagakaDescriptInfo.Ghost>;
async descriptInfoByType(type: "balloon"): Promise<UkagakaDescriptInfo.Balloon>;
async descriptInfoByType(type: "shell"): Promise<UkagakaDescriptInfo.Shell>;
async descriptInfoByType(type: "plugin"): Promise<UkagakaDescriptInfo.Plugin>;
async descriptInfoByType(type: "headline"): Promise<UkagakaDescriptInfo.Headline>;
async descriptInfoByType(type: "calendar.skin"): Promise<UkagakaDescriptInfo.CalendarSkin>;
async descriptInfoByType(type: "calendar.plugin"): Promise<UkagakaDescriptInfo.CalendarPlugin>;
async descriptInfoByType(type: UkagakaContainerStandaloneType) {
return this.descriptInfoByTypeSync(type as "ghost") as UkagakaDescriptInfo;
}
async descriptInfo() {
return this.descriptInfoSync();
}
descriptInfoByTypeSync(type: "ghost"): UkagakaDescriptInfo.Ghost;
descriptInfoByTypeSync(type: "balloon"): UkagakaDescriptInfo.Balloon;
descriptInfoByTypeSync(type: "shell"): UkagakaDescriptInfo.Shell;
descriptInfoByTypeSync(type: "plugin"): UkagakaDescriptInfo.Plugin;
descriptInfoByTypeSync(type: "headline"): UkagakaDescriptInfo.Headline;
descriptInfoByTypeSync(type: "calendar.skin"): UkagakaDescriptInfo.CalendarSkin;
descriptInfoByTypeSync(type: "calendar.plugin"): UkagakaDescriptInfo.CalendarPlugin;
descriptInfoByTypeSync(type: UkagakaContainerStandaloneType) {
return UkagakaDescriptInfo.parse(
(this.descriptTxt() as NanikaContainerSyncFile).readFileSync(),
type as "ghost", // TODO: Narazaka: 他に方法あるかな?
) as UkagakaDescriptInfo;
}
descriptInfoSync() {
return UkagakaDescriptInfo.parse(
(this.descriptTxt() as NanikaContainerSyncFile).readFileSync(),
) as UkagakaDescriptInfo;
}
async name() {
return (await this.descriptInfo()).name;
}
nameSync() {
return this.descriptInfoSync().name;
}
private _makeIndexes() {
for (let i = 0; i < this._childrenAllCache.length; ++i) {
this._indexes[this._childrenAllCache[i].path] = i;
}
}
}
/**
* statsと内容をキャッシュした -Sync API を提供するオブジェクト。
*
* entry object which provides -Sync APIs by cache all children's stats and contents.
*/
export class NanikaContainerSyncFile extends NanikaContainerSyncEntry {
path: string;
private _content: Buffer | null;
private _stats: fs.Stats | null;
/**
* @param path パス
* @param content 内容のキャッシュ
* @param stats statsのキャッシュ
*/
// tslint:disable-next-line no-shadowed-variable
constructor(path: string, content: Buffer | null = null, stats: fs.Stats | null = null) {
super(path);
this._content = content;
this._stats = stats;
}
lstatSync() {
if (this._stats == null) throw new Error("not found");
return this._stats;
}
isFileSync() { return this.lstatSync().isFile(); }
isDirectorySync() { return this.lstatSync().isDirectory(); }
isBlockDeviceSync() { return this.lstatSync().isBlockDevice(); }
isCharacterDeviceSync() { return this.lstatSync().isCharacterDevice(); }
isSymbolicLinkSync() { return this.lstatSync().isSymbolicLink(); }
isFIFOSync() { return this.lstatSync().isFIFO(); }
isSocketSync() { return this.lstatSync().isSocket(); }
async lstat() {
return this.lstatSync();
}
async isFile() { return this.isFileSync(); }
async isDirectory() { return this.isDirectorySync(); }
async isBlockDevice() { return this.isBlockDeviceSync(); }
async isCharacterDevice() { return this.isCharacterDeviceSync(); }
async isSymbolicLink() { return this.isSymbolicLinkSync(); }
async isFIFO() { return this.isFIFOSync(); }
async isSocket() { return this.isSocketSync(); }
async exists() { return this.existsSync(); }
existsSync() { return Boolean(this._content); }
readFileSync(encoding: string): string;
readFileSync(options: { encoding: string; flag?: string; }): string; // tslint:disable-line unified-signatures
readFileSync(options?: { flag?: string; }): Buffer;
readFileSync(options?: any): string | Buffer {
if (this._content == null) throw new Error("not found");
const encoding = options == null ? undefined :
typeof options === "string" ? options :
options.encoding as string | undefined;
if (encoding) {
return (this._content as Buffer).toString(encoding);
} else {
return this._content as Buffer;
}
}
readFile(encoding: string): Promise<string>;
readFile(options: { encoding: string; flag?: string; }): Promise<string>; // tslint:disable-line unified-signatures
readFile(options?: { flag?: string; }): Promise<Buffer>;
async readFile(
arg: undefined | string | { encoding: string; flag?: string; } | { flag?: string; },
): Promise<string | Buffer> {
return this.readFileSync(arg as string);
}
}