diff --git a/package.json b/package.json index 9f1d5c3..6e99727 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "start": "yarn build:dev && node dist/src/index.js", "start:dev": "yarn build:dev && nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts", "start:prod": "yarn build && node dist/src/index.js", + "start:cmd": "yarn build:dev && node dist/src/cmd.js", + "start:cmd:prod": "yarn build && node dist/src/cmd.js", "test": "yarn jest" }, "author": "Wheat Carrier", diff --git a/src/api/client.ts b/src/api/client.ts index a7f3273..48ba092 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -15,13 +15,17 @@ export class Client { metadata: TGFSMetadata; constructor( - public readonly client: TelegramClient, + protected readonly client: TelegramClient, private readonly privateChannelId: string, private readonly publicChannelId?: string, ) {} public async init() { this.metadata = await this.getMetadata(); + if (!this.metadata) { + this.metadata = new TGFSMetadata(); + await this.createEmptyDirectory(); + } } private async send(message: string) { @@ -42,6 +46,11 @@ export class Client { ); } + public async getFileInfo(fileRef: TGFSFileRef): Promise { + const file = await this.getFileFromFileRef(fileRef); + return file; + } + private async downloadMediaById( messageIds: number, downloadParams?: DownloadMediaInterface, @@ -88,8 +97,7 @@ export class Client { )[0]; if (!pinnedMessage) { - await this.createEmptyDirectory(); - return this.metadata; + return null; } const metadata = TGFSMetadata.fromObject( @@ -114,16 +122,24 @@ export class Client { private async updateMetadata() { const buffer = Buffer.from(JSON.stringify(this.metadata.toObject())); - return await this.client.editMessage(this.privateChannelId, { - message: this.metadata.msgId, - file: new CustomFile('metadata.json', buffer.length, '', buffer), - }); + const file = new CustomFile('metadata.json', buffer.length, '', buffer); + if (this.metadata.msgId) { + return await this.client.editMessage(this.privateChannelId, { + message: this.metadata.msgId, + file, + }); + } else { + const message = await this.client.sendMessage(this.privateChannelId, { + file, + }); + this.metadata.msgId = message.id; + await this.client.pinMessage(this.privateChannelId, message.id); + return message; + } } public async createEmptyDirectory() { - this.metadata = new TGFSMetadata(); this.metadata.dir = new TGFSDirectory('root', null, []); - await this.syncMetadata(); return this.metadata.dir; diff --git a/src/auth/as-user.ts b/src/auth/as-user.ts index 91261a1..4e4f7fb 100644 --- a/src/auth/as-user.ts +++ b/src/auth/as-user.ts @@ -1,6 +1,6 @@ import * as input from 'input'; -import { Logger } from 'src/utils/logger'; +import { Logger } from '../utils/logger'; import { login } from './login'; export const loginAsUser = login(async (client) => { diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..08a9907 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,2 @@ +export { loginAsBot } from './as-bot'; +export { loginAsUser } from './as-user'; diff --git a/src/cmd.ts b/src/cmd.ts new file mode 100644 index 0000000..713998a --- /dev/null +++ b/src/cmd.ts @@ -0,0 +1,42 @@ +import { hideBin } from 'yargs/helpers'; +import yargs from 'yargs/yargs'; + +import { loginAsBot } from './auth'; +import { Executor } from './commands/executor'; + +const parse = () => { + const argv: any = yargs(hideBin(process.argv)) + .command('ls ', 'list all files and directories', { + path: { + type: 'string', + description: 'path to list', + }, + }) + .command('mkdir ', 'create a directory', { + path: { + type: 'string', + description: 'path to create', + }, + }) + .demandCommand(1, 'You need at least one command before moving on') + .help().argv; + + return argv; +}; + +(async () => { + try { + const client = await loginAsBot(); + + await client.init(); + + const executor = new Executor(client); + + const argv = parse(); + await executor.execute(argv); + } catch (err) { + console.log(err.message); + } finally { + process.exit(0); + } +})(); diff --git a/src/commands/executor.ts b/src/commands/executor.ts new file mode 100644 index 0000000..dc98931 --- /dev/null +++ b/src/commands/executor.ts @@ -0,0 +1,20 @@ +import { Client } from '../api'; +import { ls } from './ls'; +import { mkdir } from './mkdir'; + +export class Executor { + constructor(private readonly client: Client) {} + + async execute(argv: any) { + let rsp: any; + + if (argv._[0] === 'ls') { + rsp = await ls(this.client)(argv.path); + } else if (argv._[0] === 'mkdir') { + rsp = await mkdir(this.client)(argv.path); + } else if (argv._[0] === '') { + } + + console.log(rsp); + } +} diff --git a/src/commands/get.ts b/src/commands/get.ts deleted file mode 100644 index 72a01b7..0000000 --- a/src/commands/get.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function get(messageId: string) { - -} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts deleted file mode 100644 index 4f77573..0000000 --- a/src/commands/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { hideBin } from 'yargs/helpers'; -import yargs from 'yargs/yargs'; - -import { ls } from './ls'; - -const argv: any = yargs(hideBin(process.argv)) - .command('ls', 'list all files', (yargs) => { - return yargs.option('path', { - describe: 'Path to list files from', - demandOption: true, - type: 'string', - }); - }) - .demandCommand(1, 'You need at least one command before moving on') - .help().argv; - -if (argv._[0] === 'ls') { - ls(argv.path); -} else if (argv._[0] === 'get') { -} else if (argv._[0] === 'send') { -} diff --git a/src/commands/ls.ts b/src/commands/ls.ts index 5fed343..07a5785 100644 --- a/src/commands/ls.ts +++ b/src/commands/ls.ts @@ -1,5 +1,44 @@ import { PathLike } from 'fs'; -export const ls = (path: PathLike) => { - console.log(path); +import { Client } from '../api'; +import { FileOrDirectoryDoesNotExistError } from '../errors/directory'; +import { TGFSFileRef } from '../model/directory'; +import { navigateToDir } from './navigate-to-dir'; + +const fileInfo = async (client: Client, fileRef: TGFSFileRef) => { + const info = await client.getFileInfo(fileRef); + const head = `${info.name}, ${Object.keys(info.versions).length} versions`; + const versions = Object.entries(info.versions).map(([id, version]) => { + return `${id}: updated at ${version.updatedAt}`; + }); + return [head, ...versions].join('\n'); +}; + +export const ls = (client: Client) => async (path: PathLike) => { + const parts = path + .toString() + .split('/') + .filter((part) => part !== ''); + let dir = await navigateToDir(client)( + parts.slice(0, parts.length - 1).join('/'), + ); + + let nextDir = dir; + + if (parts.length > 0) { + nextDir = dir.children.find((d) => d.name === parts[parts.length - 1]); + } + if (nextDir) { + return nextDir.children + .map((c) => c.name) + .concat(dir.files?.map((f) => f.name)) + .join(' '); + } else { + const nextFile = dir.files?.find((f) => f.name === parts[parts.length - 1]); + if (nextFile) { + return fileInfo(client, nextFile); + } else { + throw new FileOrDirectoryDoesNotExistError(path.toString()); + } + } }; diff --git a/src/commands/mkdir.ts b/src/commands/mkdir.ts new file mode 100644 index 0000000..8a50021 --- /dev/null +++ b/src/commands/mkdir.ts @@ -0,0 +1,13 @@ +import { Client } from '../api'; +import { navigateToDir } from './navigate-to-dir'; + +export const mkdir = (client: Client) => async (path: string) => { + const parts = path.toString().split('/'); + const dir = await navigateToDir(client)( + parts.slice(0, parts.length - 1).join('/'), + ); + + await client.createDirectoryUnder(parts[parts.length - 1], dir); + + return `created ${path}`; +}; diff --git a/src/commands/navigate-to-dir.ts b/src/commands/navigate-to-dir.ts new file mode 100644 index 0000000..2aae76a --- /dev/null +++ b/src/commands/navigate-to-dir.ts @@ -0,0 +1,27 @@ +import { TGFSDirectory } from 'src/model/directory'; + +import { Client } from '../api'; +import { FileOrDirectoryDoesNotExistError } from '../errors/directory'; + +export const navigateToDir = (client: Client) => async (path: string) => { + const pathParts = path + .toString() + .split('/') + .filter((part) => part !== ''); + + let currentDirectory = client.metadata.dir; + + for (const pathPart of pathParts) { + const directory = currentDirectory.children?.find( + (d: TGFSDirectory) => d.name === pathPart, + ); + + if (!directory) { + throw new FileOrDirectoryDoesNotExistError(path); + } + + currentDirectory = directory; + } + + return currentDirectory; +}; diff --git a/src/commands/send.ts b/src/commands/send.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/errors/directory.ts b/src/errors/directory.ts index d9ef347..4f2c7f0 100644 --- a/src/errors/directory.ts +++ b/src/errors/directory.ts @@ -1,10 +1,16 @@ import { BusinessError } from './base'; export class DirectoryAlreadyExistsError extends BusinessError { + constructor(public readonly dirName: string, public readonly cause?: string) { + super(`Directory ${dirName} already exists`, 'DIR_ALREADY_EXISTS', cause); + } +} + +export class FileOrDirectoryDoesNotExistError extends BusinessError { constructor(public readonly dirName: string, public readonly cause?: string) { super( - `Directory ${dirName} already exists`, - 'DIRECTORY_ALREADY_EXISTS', + `No such file or directory: ${dirName}`, + 'FILE_OR_DIR_DOES_NOT_EXIST', cause, ); } diff --git a/src/errors/error-codes.ts b/src/errors/error-codes.ts index fe14ca2..401eef0 100644 --- a/src/errors/error-codes.ts +++ b/src/errors/error-codes.ts @@ -2,5 +2,6 @@ export type ErrorCodes = | 'UNKNOWN' | 'EMPTY_FILE' | 'FILE_IS_EMPTY' - | 'DIRECTORY_ALREADY_EXISTS' - | 'FILE_ALREADY_EXISTS'; + | 'DIR_ALREADY_EXISTS' + | 'FILE_ALREADY_EXISTS' + | 'FILE_OR_DIR_DOES_NOT_EXIST';