refactor api

This commit is contained in:
Wheat Carrier 2024-07-10 00:58:00 +08:00
parent ae5c6e5f70
commit 9164b200da
19 changed files with 140 additions and 186 deletions

View File

@ -10,9 +10,11 @@ import { MetaDataApi } from './metadata-api';
export class DirectoryApi {
constructor(private metadataApi: MetaDataApi) {}
public root() {
return this.metadataApi.getRootDirectory();
}
public async createDirectory(
public async create(
where: { name: string; under: TGFSDirectory },
dir?: TGFSDirectory,
) {
@ -38,14 +40,14 @@ export class DirectoryApi {
return [...dir.findDirs(), ...dir.findFiles()];
}
public async deleteEmptyDirectory(directory: TGFSDirectory) {
public async rmEmpty(directory: TGFSDirectory) {
if (directory.findDirs().length > 0 || directory.findFiles().length > 0) {
throw new DirectoryIsNotEmptyError();
}
await this.dangerouslyDeleteDirectory(directory);
await this.rmDangerously(directory);
}
public async dangerouslyDeleteDirectory(directory: TGFSDirectory) {
public async rmDangerously(directory: TGFSDirectory) {
directory.delete();
await this.metadataApi.syncMetadata();
}

View File

@ -1,6 +1,6 @@
import { FileDescAPIResponse } from 'src/api/client/model';
import { TGFSFileRef } from 'src/model/directory';
import { TGFSFile } from 'src/model/file';
import { TGFSFile, TGFSFileVersion } from 'src/model/file';
import { GeneralFileMessage, isFileMessageEmpty } from './model';
import { FileRepository } from './repository/impl/file';
@ -31,6 +31,13 @@ export class FileDescApi {
return await this.fdRepo.get(fr);
}
public async *downloadFileAtVersion(
asName: string,
version: TGFSFileVersion,
): AsyncGenerator<Buffer> {
return this.fileRepo.downloadFile(asName, version.messageId);
}
public async addFileVersion(
fr: TGFSFileRef,
fileMsg: GeneralFileMessage,

View File

@ -1,5 +1,5 @@
import { TGFSDirectory, TGFSFileRef } from 'src/model/directory';
import { TGFSFile } from 'src/model/file';
import { TGFSFile, TGFSFileVersion } from 'src/model/file';
import { validateName } from 'src/utils/validate-name';
import { FileDescApi } from './file-desc-api';
@ -12,7 +12,7 @@ export class FileRefApi {
private readonly fileDescApi: FileDescApi,
) {}
public async copyFile(
public async copy(
where: TGFSDirectory,
fr: TGFSFileRef,
name?: string,
@ -22,7 +22,7 @@ export class FileRefApi {
return copiedFR;
}
private async createFile(
private async create(
where: TGFSDirectory,
fileMsg: GeneralFileMessage,
): Promise<TGFSFile> {
@ -45,7 +45,7 @@ export class FileRefApi {
}
}
private async updateFile(
private async update(
fr: TGFSFileRef,
fileMsg: GeneralFileMessage,
versionId?: string,
@ -57,7 +57,7 @@ export class FileRefApi {
return fd;
}
public async deleteFile(fr: TGFSFileRef, version?: string): Promise<void> {
public async rm(fr: TGFSFileRef, version?: string): Promise<void> {
if (!version) {
fr.delete();
await this.metadataApi.syncMetadata();
@ -70,7 +70,7 @@ export class FileRefApi {
}
}
public async uploadFile(
public async upload(
where: {
under: TGFSDirectory;
versionId?: string;
@ -79,9 +79,34 @@ export class FileRefApi {
): Promise<TGFSFile> {
const fr = where.under.findFiles([fileMsg.name])[0];
if (fr) {
return await this.updateFile(fr, fileMsg, where.versionId);
return await this.update(fr, fileMsg, where.versionId);
} else {
return await this.createFile(where.under, fileMsg);
return await this.create(where.under, fileMsg);
}
}
public async *retrieve(
fr: TGFSFileRef,
asName?: string,
): AsyncGenerator<Buffer> {
const fd = await this.desc(fr);
if (fd.isEmptyFile()) {
yield Buffer.from('');
} else {
const version = fd.getLatest();
yield* this.fileDescApi.downloadFileAtVersion(asName ?? fr.name, version);
}
}
public async *retrieveVersion(
version: TGFSFileVersion,
asName: string,
): AsyncGenerator<Buffer> {
yield* this.fileDescApi.downloadFileAtVersion(asName, version);
}
public async desc(fr: TGFSFileRef): Promise<TGFSFile> {
return await this.fileDescApi.getFileDesc(fr);
}
}

View File

@ -1,32 +1,27 @@
import { gramjs, telegraf } from 'src/api/impl';
import { config } from 'src/config';
import { TGFSDirectory, TGFSFileRef } from 'src/model/directory';
import { TGFSFile } from 'src/model/file';
import { DirectoryApi } from './directory-api';
import { FileDescApi } from './file-desc-api';
import { FileRefApi } from './file-ref-api';
import { MessageApi } from './message-api';
import { MetaDataApi } from './metadata-api';
import { GeneralFileMessage } from './model';
import { TGMsgFDRepository } from './repository/impl/fd/tg-msg';
import { FileRepository } from './repository/impl/file';
import { MetadataRepository } from './repository/impl/metadata/tg-msg';
import { JSONMetadataRepository } from './repository/impl/metadata/tg-msg';
export const createClient = async () => {
const api = {
tdlib: {
const msgApi = new MessageApi(
{
account: new gramjs.GramJSApi(await gramjs.loginAsAccount(config)),
bot: new gramjs.GramJSApi(await gramjs.loginAsBot(config)),
},
bot: new telegraf.TelegrafApi(telegraf.createBot(config)),
};
const msgApi = new MessageApi(api.tdlib, api.bot);
new telegraf.TelegrafApi(telegraf.createBot(config)),
);
const fileRepo = new FileRepository(msgApi);
const fdRepo = new TGMsgFDRepository(msgApi);
const metadataRepo = new MetadataRepository(msgApi, fileRepo);
const metadataRepo = new JSONMetadataRepository(msgApi, fileRepo);
const fdApi = new FileDescApi(fdRepo, fileRepo);
@ -36,85 +31,19 @@ export const createClient = async () => {
const frApi = new FileRefApi(metadataApi, fdApi);
const dirApi = new DirectoryApi(metadataApi);
return new Client(metadataApi, frApi, fdApi, dirApi, fileRepo);
return new Client(metadataApi, frApi, dirApi);
};
export class Client {
file: FileRefApi;
dir: DirectoryApi;
constructor(
private readonly metadataApi: MetaDataApi,
private readonly frApi: FileRefApi,
private readonly fdApi: FileDescApi,
private readonly dirApi: DirectoryApi,
private readonly fileRepo: FileRepository,
) {}
public async *downloadLatestVersion(
fr: TGFSFileRef,
asName: string,
): AsyncGenerator<Buffer> {
const fd = await this.fdApi.getFileDesc(fr);
if (fd.isEmptyFile()) {
yield Buffer.from('');
} else {
const version = fd.getLatest();
yield* this.fileRepo.downloadFile(asName, version.messageId);
}
}
public async getFileDesc(fr: TGFSFileRef) {
return this.fdApi.getFileDesc(fr);
}
public getRootDirectory() {
return this.metadataApi.getRootDirectory();
}
public async createDirectory(
where: { name: string; under: TGFSDirectory },
dir?: TGFSDirectory,
) {
return this.dirApi.createDirectory(where, dir);
}
public findDirs(dir: TGFSDirectory) {
return dir.findDirs();
}
public async copyFile(
where: TGFSDirectory,
fr: TGFSFileRef,
name?: string,
): Promise<TGFSFileRef> {
return this.frApi.copyFile(where, fr, name);
}
public async uploadFile(
where: {
under: TGFSDirectory;
versionId?: string;
},
fileMsg?: GeneralFileMessage,
): Promise<TGFSFile> {
return this.frApi.uploadFile(where, fileMsg);
}
public async ls(
dir: TGFSDirectory,
fileName?: string,
): Promise<TGFSFileRef | Array<TGFSDirectory | TGFSFileRef>> {
return this.dirApi.ls(dir, fileName);
}
public async deleteEmptyDirectory(directory: TGFSDirectory) {
return this.dirApi.deleteEmptyDirectory(directory);
}
public async dangerouslyDeleteDirectory(directory: TGFSDirectory) {
return this.dirApi.dangerouslyDeleteDirectory(directory);
}
public async deleteFile(fr: TGFSFileRef, version?: string) {
return this.frApi.deleteFile(fr, version);
this.file = this.frApi;
this.dir = this.dirApi;
}
}

View File

@ -5,7 +5,7 @@ import { TGFSMetadata } from 'src/model/metadata';
import { FileRepository } from '../file';
export class MetadataRepository implements IMetaDataRepository {
export class JSONMetadataRepository implements IMetaDataRepository {
constructor(
private readonly msgApi: MessageApi,
private readonly fileRepo: FileRepository,

View File

@ -32,7 +32,7 @@ export const copyDir =
const [basePathTo, nameTo] = splitPath(pathTo);
const dir2 = navigateToDir(client)(basePathTo);
const res = await client.createDirectory(
const res = await client.dir.create(
{ name: nameTo ?? nameFrom, under: dir2 },
dirToCopy,
);

View File

@ -28,6 +28,6 @@ export const copyFile =
const [basePathTo, nameTo] = splitPath(pathTo);
const dir2 = navigateToDir(client)(basePathTo);
const res = await client.copyFile(dir2, frToCopy, nameTo ?? nameFrom);
const res = await client.file.copy(dir2, frToCopy, nameTo ?? nameFrom);
return { from: frToCopy, to: res };
};

View File

@ -11,14 +11,14 @@ export const createDir =
if (!parents) {
const [basePath, name] = splitPath(path);
const dir = navigateToDir(client)(basePath);
return await client.createDirectory({ name: name, under: dir });
return await client.dir.create({ name: name, under: dir });
} else {
if (!path.startsWith('/')) {
throw new RelativePathError(path);
}
const paths = path.split('/').filter((p) => p);
let currentDir = client.getRootDirectory();
let currentDir = client.dir.root();
for (const p of paths) {
const children = currentDir.findDirs([p]);
if (children.length > 0) {
@ -26,7 +26,7 @@ export const createDir =
continue;
}
const dir = await client.createDirectory({
const dir = await client.dir.create({
name: p,
under: currentDir,
});

View File

@ -10,6 +10,6 @@ export const createEmptyFile = (client: Client) => async (path: PathLike) => {
const dir = navigateToDir(client)(basePath);
if (!existsSync(path)) {
return await client.uploadFile({ under: dir }, { name, empty: true });
return await client.file.upload({ under: dir }, { name, empty: true });
}
};

View File

@ -8,21 +8,21 @@ import { splitPath } from './utils';
export const list =
(client: Client) =>
async (
path: PathLike,
): Promise<TGFSFileRef | Array<TGFSFileRef | TGFSDirectory>> => {
const [basePath, name] = splitPath(path);
const dir = navigateToDir(client)(basePath);
async (
path: PathLike,
): Promise<TGFSFileRef | Array<TGFSFileRef | TGFSDirectory>> => {
const [basePath, name] = splitPath(path);
const dir = navigateToDir(client)(basePath);
let nextDir = dir;
let nextDir = dir;
if (name) {
nextDir = dir.findDir(name);
}
if (nextDir) {
return client.ls(nextDir);
} else {
// cannot find a sub-directory with the given name, so assume it's a file
return client.ls(dir, name);
}
};
if (name) {
nextDir = dir.findDir(name);
}
if (nextDir) {
return client.dir.ls(nextDir);
} else {
// cannot find a sub-directory with the given name, so assume it's a file
return client.dir.ls(dir, name);
}
};

View File

@ -6,5 +6,5 @@ export const moveDir =
(client: Client) => async (pathFrom: string, pathTo: string) => {
const { from, to } = await copyDir(client)(pathFrom, pathTo);
await client.dangerouslyDeleteDirectory(from);
await client.dir.rmDangerously(from);
};

View File

@ -5,5 +5,5 @@ import { copyFile } from './copy-file';
export const moveFile =
(client: Client) => async (pathFrom: string, pathTo: string) => {
const { from, to } = await copyFile(client)(pathFrom, pathTo);
await client.deleteFile(from);
await client.file.rm(from);
};

View File

@ -7,7 +7,7 @@ export const navigateToDir = (client: Client) => (path: string) => {
.split('/')
.filter((part) => part !== '');
let currentDirectory = client.getRootDirectory();
let currentDirectory = client.dir.root();
for (const pathPart of pathParts) {
const directory = currentDirectory.findDirs([pathPart])[0];

View File

@ -11,12 +11,12 @@ export const removeDir =
if (!recursive) {
const child = dir.findDirs([name])[0];
if (child) {
await client.deleteEmptyDirectory(child);
await client.dir.rmEmpty(child);
} else {
throw new FileOrDirectoryDoesNotExistError(path, `remove dir ${path}`);
}
} else {
const nextDir = name ? dir.findDirs([name])[0] : dir;
await client.dangerouslyDeleteDirectory(nextDir);
await client.dir.rmDangerously(nextDir);
}
};

View File

@ -9,7 +9,7 @@ export const removeFile = (client: Client) => async (path: string) => {
const dir = navigateToDir(client)(basePath);
const fileRef = dir.findFiles([name])[0];
if (fileRef) {
await client.deleteFile(fileRef);
await client.file.rm(fileRef);
} else {
throw new FileOrDirectoryDoesNotExistError(path, `remove file ${path}`);
}

View File

@ -18,7 +18,7 @@ export const uploadFromLocal =
throw new FileOrDirectoryDoesNotExistError(path, `upload from ${path}`);
}
return await client.uploadFile(
return await client.file.upload(
{ under: dir },
{ name, path: local.toString() },
);
@ -30,7 +30,7 @@ export const uploadFromBytes =
const dir = navigateToDir(client)(basePath);
return await client.uploadFile({ under: dir }, { name, buffer: bytes });
return await client.file.upload({ under: dir }, { name, buffer: bytes });
};
export const uploadFromStream =
@ -40,5 +40,5 @@ export const uploadFromStream =
const dir = navigateToDir(client)(basePath);
return await client.uploadFile({ under: dir }, { name, stream, size });
return await client.file.upload({ under: dir }, { name, stream, size });
};

View File

@ -16,7 +16,7 @@ export const ls = (client: Client) => async (argv: { path: PathLike }) => {
})
.join(' ');
} else {
const fd = await client.getFileDesc(res);
const fd = await client.file.desc(res);
return fileInfo(client, fd);
}
};

View File

@ -152,7 +152,7 @@ export class TGFSFileSystem extends VirtualFileSystem {
): Promise<TGFSFileRef | (TGFSFileRef | TGFSDirectory)[]> {
const res = await list(this.tgClient)(path.toString());
if (res instanceof TGFSFileRef) {
const fd = await this.tgClient.getFileDesc(res);
const fd = await this.tgClient.file.desc(res);
this.resources[path.toString()] = TGFSFileResource.fromFileDesc(fd);
} else {
this.resources[path.toString()] = new TGFSDirResource();
@ -169,7 +169,7 @@ export class TGFSFileSystem extends VirtualFileSystem {
.filter((res) => res instanceof TGFSFileRef)
.map((fr) => {
return (async () => {
const fd = await this.tgClient.getFileDesc(fr as TGFSFileRef);
const fd = await this.tgClient.file.desc(fr as TGFSFileRef);
if (path.toString() === '/') {
this.resources[`/${fr.name}`] = TGFSFileResource.fromFileDesc(fd);
} else {
@ -315,10 +315,7 @@ export class TGFSFileSystem extends VirtualFileSystem {
const fileRef = (await list(this.tgClient)(
path.toString(),
)) as TGFSFileRef;
const chunks = this.tgClient.downloadLatestVersion(
fileRef,
fileRef.name,
);
const chunks = this.tgClient.file.retrieve(fileRef, fileRef.name);
callback(null, Readable.from(chunks));
} catch (err) {

View File

@ -20,38 +20,38 @@ describe('file and directory operations', () => {
});
it('should create a directory', async () => {
const root = client.getRootDirectory();
const d1 = await client.createDirectory({ name: 'd1', under: root });
const root = client.dir.root();
const d1 = await client.dir.create({ name: 'd1', under: root });
expect(root.findDirs(['d1'])[0]).toEqual(d1);
});
it('should throw an error if the directory name is illegal', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
await expect(
client.createDirectory({ name: '-d1', under: root }),
client.dir.create({ name: '-d1', under: root }),
).rejects.toThrow();
await expect(
client.createDirectory({ name: 'd/1', under: root }),
client.dir.create({ name: 'd/1', under: root }),
).rejects.toThrow();
});
it('should remove a directory', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
const d1 = await client.createDirectory({ name: 'd1', under: root });
await client.dangerouslyDeleteDirectory(d1);
const d1 = await client.dir.create({ name: 'd1', under: root });
await client.dir.rmDangerously(d1);
expect(root.findDirs(['d1'])[0]).toBeUndefined();
});
it('should remove all directories', async () => {
const d1 = await client.createDirectory({
const d1 = await client.dir.create({
name: 'd1',
under: client.getRootDirectory(),
under: client.dir.root(),
});
await client.createDirectory({ name: 'd2', under: d1 });
await client.dangerouslyDeleteDirectory(client.getRootDirectory());
expect(client.getRootDirectory().findDirs(['d1'])[0]).toBeUndefined();
await client.dir.create({ name: 'd2', under: d1 });
await client.dir.rmDangerously(client.dir.root());
expect(client.dir.root().findDirs(['d1'])[0]).toBeUndefined();
});
});
@ -63,8 +63,8 @@ describe('file and directory operations', () => {
});
it('should create a small file from buffer', async () => {
const root = client.getRootDirectory();
const f1 = await client.uploadFile(
const root = client.dir.root();
const f1 = await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from('mock-file-content') },
);
@ -75,8 +75,8 @@ describe('file and directory operations', () => {
const fileName = `${Math.random()}.txt`;
fs.writeFileSync(fileName, 'mock-file-content');
const root = client.getRootDirectory();
const f1 = await client.uploadFile(
const root = client.dir.root();
const f1 = await client.file.upload(
{ under: root },
{ name: 'f1', path: fileName },
);
@ -88,9 +88,9 @@ describe('file and directory operations', () => {
it('should create a big file from buffer', async () => {
const content = Buffer.alloc(1024 * 1024 * 10, 'a');
const root = client.getRootDirectory();
const root = client.dir.root();
const f1 = await client.uploadFile(
const f1 = await client.file.upload(
{ under: root },
{ name: 'f1', buffer: content },
);
@ -102,9 +102,9 @@ describe('file and directory operations', () => {
const content = Buffer.alloc(1024 * 1024 * 10, 'a');
fs.writeFileSync(fileName, content);
const root = client.getRootDirectory();
const root = client.dir.root();
const f1 = await client.uploadFile(
const f1 = await client.file.upload(
{ under: root },
{ name: 'f1', path: fileName },
);
@ -114,47 +114,43 @@ describe('file and directory operations', () => {
});
it('should add a file version', async () => {
const root = client.getRootDirectory();
await client.uploadFile(
const root = client.dir.root();
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from('mock-file-content') },
);
await sleep(300); // wait for the timestamp to change to ensure the order of versions
const content2 = 'mock-file-content-edited';
await client.uploadFile(
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from(content2) },
);
const fr = root.findFiles(['f1'])[0];
const fd = await client.getFileDesc(fr);
const fd = await client.file.desc(fr);
expect(Object.keys(fd.versions)).toHaveLength(2);
const content = await saveToBuffer(
client.downloadLatestVersion(fr, 'f1'),
);
const content = await saveToBuffer(client.file.retrieve(fr, 'f1'));
expect(content.toString()).toEqual(content2);
});
it('should edit a file version', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
await client.uploadFile(
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from('mock-file-content') },
);
const content2 = 'mock-file-content-edited';
let fr = root.findFiles(['f1'])[0];
const fd = await client.getFileDesc(fr);
const fd = await client.file.desc(fr);
await client.uploadFile(
await client.file.upload(
{ under: root, versionId: fd.latestVersionId },
{ name: 'f1', buffer: Buffer.from(content2) },
);
const content = await saveToBuffer(
client.downloadLatestVersion(fr, 'f1'),
);
const content = await saveToBuffer(client.file.retrieve(fr, 'f1'));
expect(content.toString()).toEqual(content2);
});
@ -176,48 +172,46 @@ describe('file and directory operations', () => {
// });
it('should remove a file', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
const f1 = await client.uploadFile(
const f1 = await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from('mock-file-content') },
);
const fr = root.findFiles(['f1'])[0];
await client.deleteFile(fr);
await client.file.rm(fr);
expect(root.findFiles(['f1'])[0]).toBeUndefined();
});
it('should remove a file version', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
const content = 'mock-file-content';
await client.uploadFile(
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from(content) },
);
await sleep(300);
await client.uploadFile(
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from('mock-file-content-edited') },
);
const fr = root.findFiles(['f1'])[0];
let fd = await client.getFileDesc(fr);
let fd = await client.file.desc(fr);
await client.deleteFile(fr, fd.latestVersionId);
await client.file.rm(fr, fd.latestVersionId);
fd = await client.getFileDesc(fr);
fd = await client.file.desc(fr);
expect(Object.keys(fd.versions)).toHaveLength(1);
const content2 = await saveToBuffer(
client.downloadLatestVersion(fr, 'f1'),
);
const content2 = await saveToBuffer(client.file.retrieve(fr, 'f1'));
expect(content2.toString()).toEqual(content);
});
it('should download a file as a local file', async () => {
const root = client.getRootDirectory();
const root = client.dir.root();
const content = 'mock-file-content';
await client.uploadFile(
await client.file.upload(
{ under: root },
{ name: 'f1', buffer: Buffer.from(content) },
);
@ -225,7 +219,7 @@ describe('file and directory operations', () => {
const fr = root.findFiles(['f1'])[0];
const localFileName = `${Math.random()}.txt`;
await saveToFile(client.downloadLatestVersion(fr, 'f1'), localFileName);
await saveToFile(client.file.retrieve(fr, 'f1'), localFileName);
const contentRead = fs.readFileSync(localFileName);
expect(contentRead.toString()).toEqual(content);