mirror of
https://github.com/TheodoreKrypton/tgfs
synced 2024-11-21 14:41:40 +00:00
refactor client api
This commit is contained in:
parent
0b0a5ad098
commit
a654739090
@ -1,3 +1,4 @@
|
||||
.vscode/
|
||||
coverage/
|
||||
node_modules/
|
||||
dist/
|
@ -4,6 +4,7 @@
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"],
|
||||
"importOrder": ["^[^(\\.|src/)]", "^src/", "^[\\.]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
|
@ -75,7 +75,6 @@ Tested WebDAV Clients:
|
||||
$ tgfs cmd rm -r /some-folder
|
||||
```
|
||||
|
||||
|
||||
## Step by Step Guide to Set up config
|
||||
|
||||
### Automatically:
|
||||
@ -96,14 +95,15 @@ A config file will be auto-generated when you run the program for the first time
|
||||
2. There should be a message like "Channel created". Right click the message and copy the post link.
|
||||
3. The format of the link should be like `https://t.me/c/1234567/1`, where `1234567` is the channel id. Copy the channel id to the config file (`telegram -> private_file_channel`)
|
||||
|
||||
|
||||
## Config fields explanation
|
||||
|
||||
- telegram
|
||||
|
||||
- session_file: The file path to store the session data. If you want to use multiple accounts, you can set different session files for each account.
|
||||
- login_timeout: Time to wait before login attempt aborts (in milliseconds).
|
||||
|
||||
- tgfs
|
||||
|
||||
- download
|
||||
- porgress: Whether to show a progress bar when downloading files
|
||||
- chunk_size_kb: The chunk size in KB when downloading files. Bigger chunk size means less requests.
|
||||
|
@ -26,7 +26,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
@ -37,7 +37,7 @@
|
||||
"@types/yargs": "^17.0.24",
|
||||
"jest": "^29.6.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier": "^3.0.2",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
|
@ -1,313 +0,0 @@
|
||||
import cliProgress from 'cli-progress';
|
||||
|
||||
import fs from 'fs';
|
||||
import { Api, TelegramClient } from 'telegram';
|
||||
import { IterDownloadFunction } from 'telegram/client/downloads';
|
||||
import { CustomFile } from 'telegram/client/uploads';
|
||||
import { FileLike } from 'telegram/define';
|
||||
|
||||
import { config } from '../config';
|
||||
import { TechnicalError } from '../errors/base';
|
||||
import { DirectoryIsNotEmptyError } from '../errors/path';
|
||||
import { TGFSDirectory, TGFSFileRef } from '../model/directory';
|
||||
import { TGFSFile } from '../model/file';
|
||||
import { TGFSMetadata } from '../model/metadata';
|
||||
import { validateName } from '../utils/validate-name';
|
||||
|
||||
export class Client {
|
||||
private metadata: TGFSMetadata;
|
||||
|
||||
constructor(
|
||||
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) {
|
||||
return await this.client.sendMessage(this.privateChannelId, {
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
private async getMessagesByIds(messageIds: number[]) {
|
||||
return await this.client.getMessages(this.privateChannelId, {
|
||||
ids: messageIds,
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectsByMessageIds(messageIds: number[]) {
|
||||
return (await this.getMessagesByIds(messageIds)).map((message) =>
|
||||
JSON.parse(message.text),
|
||||
);
|
||||
}
|
||||
|
||||
public async getFileInfo(fileRef: TGFSFileRef): Promise<TGFSFile> {
|
||||
const file = await this.getFileFromFileRef(fileRef);
|
||||
|
||||
const versions = Object.values(file.versions);
|
||||
|
||||
const fileMessages = await this.getMessagesByIds(
|
||||
versions.map((version) => version.messageId),
|
||||
);
|
||||
|
||||
versions.forEach((version, i) => {
|
||||
const fileMessage = fileMessages[i];
|
||||
version.size = Number(fileMessage.document.size);
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private async downloadMediaByMessageId(
|
||||
file: { name: string; messageId: number },
|
||||
withProgressBar?: boolean,
|
||||
options?: IterDownloadFunction,
|
||||
) {
|
||||
const message = (await this.getMessagesByIds([file.messageId]))[0];
|
||||
|
||||
const fileSize = Number(message.document.size);
|
||||
const chunkSize = config.tgfs.download.chunksize * 1024;
|
||||
|
||||
let pgBar: cliProgress.SingleBar;
|
||||
if (withProgressBar) {
|
||||
pgBar = new cliProgress.SingleBar({
|
||||
format: `${file.name} [{bar}] {percentage}%`,
|
||||
});
|
||||
pgBar.start(fileSize, 0);
|
||||
}
|
||||
|
||||
const buffer = Buffer.alloc(fileSize);
|
||||
let i = 0;
|
||||
for await (const chunk of this.client.iterDownload({
|
||||
file: new Api.InputDocumentFileLocation({
|
||||
id: message.document.id,
|
||||
accessHash: message.document.accessHash,
|
||||
fileReference: message.document.fileReference,
|
||||
thumbSize: '',
|
||||
}),
|
||||
requestSize: chunkSize,
|
||||
})) {
|
||||
chunk.copy(buffer, i * chunkSize, 0, Number(chunk.length));
|
||||
i += 1;
|
||||
|
||||
if (withProgressBar) {
|
||||
pgBar.update(i * chunkSize);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public async downloadFileAtVersion(
|
||||
fileRef: TGFSFileRef,
|
||||
outputFile?: string | fs.WriteStream,
|
||||
versionId?: string,
|
||||
): Promise<Buffer | null> {
|
||||
const tgfsFile = await this.getFileFromFileRef(fileRef);
|
||||
|
||||
const version = versionId
|
||||
? tgfsFile.getVersion(versionId)
|
||||
: tgfsFile.getLatest();
|
||||
|
||||
const res = await this.downloadMediaByMessageId(
|
||||
{ messageId: version.messageId, name: tgfsFile.name },
|
||||
true,
|
||||
);
|
||||
if (res instanceof Buffer) {
|
||||
if (outputFile) {
|
||||
if (outputFile instanceof fs.WriteStream) {
|
||||
outputFile.write(res);
|
||||
} else {
|
||||
fs.writeFile(outputFile, res, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
throw new TechnicalError(
|
||||
`Downloaded file is not a buffer. ${this.privateChannelId}/${version.messageId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getMetadata() {
|
||||
const pinnedMessage = (
|
||||
await this.client.getMessages(this.privateChannelId, {
|
||||
filter: new Api.InputMessagesFilterPinned(),
|
||||
})
|
||||
)[0];
|
||||
|
||||
if (!pinnedMessage) {
|
||||
return null;
|
||||
}
|
||||
const metadata = TGFSMetadata.fromObject(
|
||||
JSON.parse(
|
||||
String(
|
||||
await this.downloadMediaByMessageId(
|
||||
{ messageId: pinnedMessage.id, name: 'metadata.json' },
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
metadata.msgId = pinnedMessage.id;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private async sendFile(file: FileLike) {
|
||||
return await this.client.sendFile(this.privateChannelId, {
|
||||
file,
|
||||
workers: 16,
|
||||
});
|
||||
}
|
||||
|
||||
private async syncMetadata() {
|
||||
this.metadata.syncWith(await this.getMetadata());
|
||||
|
||||
await this.updateMetadata();
|
||||
}
|
||||
|
||||
private async updateMetadata() {
|
||||
const buffer = Buffer.from(JSON.stringify(this.metadata.toObject()));
|
||||
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 getRootDirectory() {
|
||||
return this.metadata.dir;
|
||||
}
|
||||
|
||||
public async createEmptyDirectory() {
|
||||
this.metadata.dir = new TGFSDirectory('root', null);
|
||||
await this.syncMetadata();
|
||||
|
||||
return this.metadata.dir;
|
||||
}
|
||||
|
||||
public async createDirectoryUnder(name: string, where: TGFSDirectory) {
|
||||
validateName(name);
|
||||
|
||||
const newDirectory = where.createChild(name);
|
||||
await this.syncMetadata();
|
||||
|
||||
return newDirectory;
|
||||
}
|
||||
|
||||
public async getFileFromFileRef(fileRef: TGFSFileRef) {
|
||||
return TGFSFile.fromObject(
|
||||
(await this.getObjectsByMessageIds([fileRef.getMessageId()]))[0],
|
||||
);
|
||||
}
|
||||
|
||||
public async newFileUnder(
|
||||
name: string,
|
||||
where: TGFSDirectory,
|
||||
file: FileLike,
|
||||
) {
|
||||
validateName(name);
|
||||
|
||||
const uploadFileMsg = await this.sendFile(file);
|
||||
|
||||
const tgfsFile = new TGFSFile(name);
|
||||
tgfsFile.addVersionFromFileMessage(uploadFileMsg);
|
||||
|
||||
const tgfsFileMsg = await this.send(JSON.stringify(tgfsFile.toObject()));
|
||||
|
||||
const tgfsFileRef = where.createFileRef(name, tgfsFileMsg);
|
||||
|
||||
await this.syncMetadata();
|
||||
|
||||
return tgfsFileRef;
|
||||
}
|
||||
|
||||
public async updateFile(
|
||||
tgfsFileRef: TGFSFileRef,
|
||||
file: FileLike,
|
||||
versionId?: string,
|
||||
) {
|
||||
const tgfsFile = await this.getFileFromFileRef(tgfsFileRef);
|
||||
|
||||
const uploadFileMsg = await this.sendFile(file);
|
||||
|
||||
if (!versionId) {
|
||||
tgfsFile.addVersionFromFileMessage(uploadFileMsg);
|
||||
} else {
|
||||
const tgfsFileVersion = tgfsFile.getVersion(versionId);
|
||||
tgfsFileVersion.messageId = uploadFileMsg.id;
|
||||
tgfsFile.updateVersion(tgfsFileVersion);
|
||||
}
|
||||
|
||||
this.client.editMessage(this.privateChannelId, {
|
||||
message: tgfsFileRef.getMessageId(),
|
||||
text: JSON.stringify(tgfsFile.toObject()),
|
||||
});
|
||||
|
||||
await this.syncMetadata();
|
||||
|
||||
return tgfsFileRef;
|
||||
}
|
||||
|
||||
public async putFileUnder(
|
||||
name: string,
|
||||
where: TGFSDirectory,
|
||||
file: FileLike,
|
||||
) {
|
||||
const tgfsFileRef = where.findFiles([name])[0];
|
||||
if (tgfsFileRef) {
|
||||
return await this.updateFile(tgfsFileRef, file);
|
||||
} else {
|
||||
return await this.newFileUnder(name, where, file);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteFileAtVersion(tgfsFileRef: TGFSFileRef, version?: string) {
|
||||
if (!version) {
|
||||
tgfsFileRef.delete();
|
||||
} else {
|
||||
const tgfsFile = await this.getFileFromFileRef(tgfsFileRef);
|
||||
tgfsFile.deleteVersion(version);
|
||||
await this.client.editMessage(this.privateChannelId, {
|
||||
message: tgfsFileRef.getMessageId(),
|
||||
text: JSON.stringify(tgfsFile.toObject()),
|
||||
});
|
||||
}
|
||||
await this.syncMetadata();
|
||||
}
|
||||
|
||||
public async deleteEmptyDirectory(directory: TGFSDirectory) {
|
||||
if (
|
||||
directory.findChildren().length > 0 ||
|
||||
directory.findFiles().length > 0
|
||||
) {
|
||||
throw new DirectoryIsNotEmptyError();
|
||||
}
|
||||
await this.deleteDirectory(directory);
|
||||
}
|
||||
|
||||
public async deleteDirectory(directory: TGFSDirectory) {
|
||||
directory.delete();
|
||||
await this.syncMetadata();
|
||||
}
|
||||
}
|
43
src/api/client/directory-api.ts
Normal file
43
src/api/client/directory-api.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { TelegramClient } from 'telegram';
|
||||
|
||||
import { DirectoryIsNotEmptyError } from '../../errors/path';
|
||||
import { TGFSDirectory } from '../../model/directory';
|
||||
import { validateName } from '../../utils/validate-name';
|
||||
import { MetaDataApi } from './metadata-api';
|
||||
|
||||
export class DirectoryApi extends MetaDataApi {
|
||||
constructor(protected readonly client: TelegramClient) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public async createRootDirectory() {
|
||||
await this.resetMetadata();
|
||||
await this.syncMetadata();
|
||||
|
||||
return this.metadata.dir;
|
||||
}
|
||||
|
||||
public async createDirectoryUnder(name: string, where: TGFSDirectory) {
|
||||
validateName(name);
|
||||
|
||||
const newDirectory = where.createChild(name);
|
||||
await this.syncMetadata();
|
||||
|
||||
return newDirectory;
|
||||
}
|
||||
|
||||
public async deleteEmptyDirectory(directory: TGFSDirectory) {
|
||||
if (
|
||||
directory.findChildren().length > 0 ||
|
||||
directory.findFiles().length > 0
|
||||
) {
|
||||
throw new DirectoryIsNotEmptyError();
|
||||
}
|
||||
await this.deleteDirectory(directory);
|
||||
}
|
||||
|
||||
public async deleteDirectory(directory: TGFSDirectory) {
|
||||
directory.delete();
|
||||
await this.syncMetadata();
|
||||
}
|
||||
}
|
121
src/api/client/file-api.ts
Normal file
121
src/api/client/file-api.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import cliProgress from 'cli-progress';
|
||||
|
||||
import fs from 'fs';
|
||||
import { Api, TelegramClient } from 'telegram';
|
||||
import { IterDownloadFunction } from 'telegram/client/downloads';
|
||||
import { FileLike } from 'telegram/define';
|
||||
|
||||
import { config } from '../../config';
|
||||
import { TechnicalError } from '../../errors/base';
|
||||
import { TGFSFileRef } from '../../model/directory';
|
||||
import { TGFSFile } from '../../model/file';
|
||||
import { MessageApi } from './message-api';
|
||||
|
||||
export class FileApi extends MessageApi {
|
||||
constructor(protected readonly client: TelegramClient) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public async getFileInfo(fileRef: TGFSFileRef): Promise<TGFSFile> {
|
||||
const file = await this.getFileFromFileRef(fileRef);
|
||||
|
||||
const versions = Object.values(file.versions);
|
||||
|
||||
const fileMessages = await this.getMessagesByIds(
|
||||
versions.map((version) => version.messageId),
|
||||
);
|
||||
|
||||
versions.forEach((version, i) => {
|
||||
const fileMessage = fileMessages[i];
|
||||
version.size = Number(fileMessage.document.size);
|
||||
});
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
protected async downloadMediaByMessageId(
|
||||
file: { name: string; messageId: number },
|
||||
withProgressBar?: boolean,
|
||||
options?: IterDownloadFunction,
|
||||
) {
|
||||
const message = (await this.getMessagesByIds([file.messageId]))[0];
|
||||
|
||||
const fileSize = Number(message.document.size);
|
||||
const chunkSize = config.tgfs.download.chunksize * 1024;
|
||||
|
||||
let pgBar: cliProgress.SingleBar;
|
||||
if (withProgressBar) {
|
||||
pgBar = new cliProgress.SingleBar({
|
||||
format: `${file.name} [{bar}] {percentage}%`,
|
||||
});
|
||||
pgBar.start(fileSize, 0);
|
||||
}
|
||||
|
||||
const buffer = Buffer.alloc(fileSize);
|
||||
let i = 0;
|
||||
for await (const chunk of this.client.iterDownload({
|
||||
file: new Api.InputDocumentFileLocation({
|
||||
id: message.document.id,
|
||||
accessHash: message.document.accessHash,
|
||||
fileReference: message.document.fileReference,
|
||||
thumbSize: '',
|
||||
}),
|
||||
requestSize: chunkSize,
|
||||
})) {
|
||||
chunk.copy(buffer, i * chunkSize, 0, Number(chunk.length));
|
||||
i += 1;
|
||||
|
||||
if (withProgressBar) {
|
||||
pgBar.update(i * chunkSize);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public async downloadFileAtVersion(
|
||||
fileRef: TGFSFileRef,
|
||||
outputFile?: string | fs.WriteStream,
|
||||
versionId?: string,
|
||||
): Promise<Buffer | null> {
|
||||
const tgfsFile = await this.getFileFromFileRef(fileRef);
|
||||
|
||||
const version = versionId
|
||||
? tgfsFile.getVersion(versionId)
|
||||
: tgfsFile.getLatest();
|
||||
|
||||
const res = await this.downloadMediaByMessageId(
|
||||
{ messageId: version.messageId, name: tgfsFile.name },
|
||||
true,
|
||||
);
|
||||
if (res instanceof Buffer) {
|
||||
if (outputFile) {
|
||||
if (outputFile instanceof fs.WriteStream) {
|
||||
outputFile.write(res);
|
||||
} else {
|
||||
fs.writeFile(outputFile, res, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return res;
|
||||
} else {
|
||||
throw new TechnicalError(
|
||||
`Downloaded file is not a buffer. ${this.privateChannelId}/${version.messageId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async sendFile(file: FileLike) {
|
||||
return await this.client.sendFile(this.privateChannelId, {
|
||||
file,
|
||||
workers: 16,
|
||||
});
|
||||
}
|
||||
|
||||
public async getFileFromFileRef(fileRef: TGFSFileRef) {
|
||||
const message = (await this.getMessagesByIds([fileRef.getMessageId()]))[0];
|
||||
return TGFSFile.fromObject(JSON.parse(message.text));
|
||||
}
|
||||
}
|
95
src/api/client/index.ts
Normal file
95
src/api/client/index.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { TelegramClient } from 'telegram';
|
||||
import { FileLike } from 'telegram/define';
|
||||
|
||||
import { TGFSDirectory, TGFSFileRef } from '../../model/directory';
|
||||
import { TGFSFile } from '../../model/file';
|
||||
import { validateName } from '../../utils/validate-name';
|
||||
import { DirectoryApi } from './directory-api';
|
||||
|
||||
export class Client extends DirectoryApi {
|
||||
constructor(protected readonly client: TelegramClient) {
|
||||
super(client);
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await this.initMetadata();
|
||||
if (!this.getRootDirectory()) {
|
||||
await this.createRootDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
public async newFileUnder(
|
||||
name: string,
|
||||
where: TGFSDirectory,
|
||||
file: FileLike,
|
||||
) {
|
||||
validateName(name);
|
||||
|
||||
const uploadFileMsg = await this.sendFile(file);
|
||||
|
||||
const tgfsFile = new TGFSFile(name);
|
||||
tgfsFile.addVersionFromFileMessage(uploadFileMsg);
|
||||
|
||||
const tgfsFileMsg = await this.send(JSON.stringify(tgfsFile.toObject()));
|
||||
|
||||
const tgfsFileRef = where.createFileRef(name, tgfsFileMsg);
|
||||
|
||||
await this.syncMetadata();
|
||||
|
||||
return tgfsFileRef;
|
||||
}
|
||||
|
||||
public async updateFile(
|
||||
tgfsFileRef: TGFSFileRef,
|
||||
file: FileLike,
|
||||
versionId?: string,
|
||||
) {
|
||||
const tgfsFile = await this.getFileFromFileRef(tgfsFileRef);
|
||||
|
||||
const uploadFileMsg = await this.sendFile(file);
|
||||
|
||||
if (!versionId) {
|
||||
tgfsFile.addVersionFromFileMessage(uploadFileMsg);
|
||||
} else {
|
||||
const tgfsFileVersion = tgfsFile.getVersion(versionId);
|
||||
tgfsFileVersion.messageId = uploadFileMsg.id;
|
||||
tgfsFile.updateVersion(tgfsFileVersion);
|
||||
}
|
||||
|
||||
this.client.editMessage(this.privateChannelId, {
|
||||
message: tgfsFileRef.getMessageId(),
|
||||
text: JSON.stringify(tgfsFile.toObject()),
|
||||
});
|
||||
|
||||
await this.syncMetadata();
|
||||
|
||||
return tgfsFileRef;
|
||||
}
|
||||
|
||||
public async putFileUnder(
|
||||
name: string,
|
||||
where: TGFSDirectory,
|
||||
file: FileLike,
|
||||
) {
|
||||
const tgfsFileRef = where.findFiles([name])[0];
|
||||
if (tgfsFileRef) {
|
||||
return await this.updateFile(tgfsFileRef, file);
|
||||
} else {
|
||||
return await this.newFileUnder(name, where, file);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteFileAtVersion(tgfsFileRef: TGFSFileRef, version?: string) {
|
||||
if (!version) {
|
||||
tgfsFileRef.delete();
|
||||
} else {
|
||||
const tgfsFile = await this.getFileFromFileRef(tgfsFileRef);
|
||||
tgfsFile.deleteVersion(version);
|
||||
await this.client.editMessage(this.privateChannelId, {
|
||||
message: tgfsFileRef.getMessageId(),
|
||||
text: JSON.stringify(tgfsFile.toObject()),
|
||||
});
|
||||
}
|
||||
await this.syncMetadata();
|
||||
}
|
||||
}
|
21
src/api/client/message-api.ts
Normal file
21
src/api/client/message-api.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { TelegramClient } from 'telegram';
|
||||
|
||||
import { config } from 'src/config';
|
||||
|
||||
export class MessageApi {
|
||||
protected readonly privateChannelId = config.telegram.private_file_channel;
|
||||
|
||||
constructor(protected readonly client: TelegramClient) {}
|
||||
|
||||
protected async send(message: string) {
|
||||
return await this.client.sendMessage(this.privateChannelId, {
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
protected async getMessagesByIds(messageIds: number[]) {
|
||||
return await this.client.getMessages(this.privateChannelId, {
|
||||
ids: messageIds,
|
||||
});
|
||||
}
|
||||
}
|
73
src/api/client/metadata-api.ts
Normal file
73
src/api/client/metadata-api.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Api } from 'telegram';
|
||||
import { CustomFile } from 'telegram/client/uploads';
|
||||
|
||||
import { TGFSDirectory } from '../../model/directory';
|
||||
import { TGFSMetadata } from '../../model/metadata';
|
||||
import { FileApi } from './file-api';
|
||||
|
||||
export class MetaDataApi extends FileApi {
|
||||
protected metadata: TGFSMetadata;
|
||||
|
||||
protected async initMetadata() {
|
||||
this.metadata = await this.getMetadata();
|
||||
if (!this.metadata) {
|
||||
this.metadata = new TGFSMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
protected async resetMetadata() {
|
||||
this.metadata.dir = new TGFSDirectory('root', null);
|
||||
}
|
||||
|
||||
protected async getMetadata() {
|
||||
const pinnedMessage = (
|
||||
await this.client.getMessages(this.privateChannelId, {
|
||||
filter: new Api.InputMessagesFilterPinned(),
|
||||
})
|
||||
)[0];
|
||||
|
||||
if (!pinnedMessage) {
|
||||
return null;
|
||||
}
|
||||
const metadata = TGFSMetadata.fromObject(
|
||||
JSON.parse(
|
||||
String(
|
||||
await this.downloadMediaByMessageId(
|
||||
{ messageId: pinnedMessage.id, name: 'metadata.json' },
|
||||
false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
metadata.msgId = pinnedMessage.id;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
protected async syncMetadata() {
|
||||
this.metadata.syncWith(await this.getMetadata());
|
||||
|
||||
await this.updateMetadata();
|
||||
}
|
||||
|
||||
protected async updateMetadata() {
|
||||
const buffer = Buffer.from(JSON.stringify(this.metadata.toObject()));
|
||||
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 getRootDirectory() {
|
||||
return this.metadata.dir;
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ export const login =
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
return new Client(client, config.telegram.private_file_channel);
|
||||
return new Client(client);
|
||||
} catch (err) {
|
||||
Logger.error(err);
|
||||
}
|
||||
@ -39,5 +39,5 @@ export const login =
|
||||
String(client.session.save()),
|
||||
);
|
||||
|
||||
return new Client(client, config.telegram.private_file_channel);
|
||||
return new Client(client);
|
||||
};
|
||||
|
@ -1,9 +1,8 @@
|
||||
import fs from 'fs';
|
||||
import input from 'input';
|
||||
import yaml from 'js-yaml';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import input from 'input';
|
||||
import { loginAsUser } from './auth';
|
||||
|
||||
export const config: any = {};
|
||||
|
||||
@ -47,7 +46,9 @@ export const loadConfig = (configPath: string) => {
|
||||
};
|
||||
|
||||
export const createConfig = async () => {
|
||||
const createNow = await input.confirm('The config file is not found. Create a config file now?')
|
||||
const createNow = await input.confirm(
|
||||
'The config file is not found. Create a config file now?',
|
||||
);
|
||||
|
||||
if (!createNow) {
|
||||
process.exit(0);
|
||||
@ -57,43 +58,59 @@ export const createConfig = async () => {
|
||||
if (answer.trim().length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return 'This field is mandatory!'
|
||||
return 'This field is mandatory!';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const config: any = {};
|
||||
|
||||
const configPath = await input.text('Where do you want to save the config file', { default: path.join(process.cwd(), "config.yaml") });
|
||||
const configPath = await input.text(
|
||||
'Where do you want to save the config file',
|
||||
{ default: path.join(process.cwd(), 'config.yaml') },
|
||||
);
|
||||
|
||||
config.telegram = {};
|
||||
|
||||
console.log('\nGo to https://my.telegram.org/apps, follow the steps to log in and paste the App api_id and App api_hash here')
|
||||
config.telegram.api_id = Number(await input.text('App api_id', { validate: validateNotEmpty }));
|
||||
config.telegram.api_hash = await input.text('App api_hash', { validate: validateNotEmpty });
|
||||
config.telegram.session_file = await input.text('Where do you want to save the session', { default: '~/.tgfs/account.session' });
|
||||
console.log(
|
||||
'\nGo to https://my.telegram.org/apps, follow the steps to log in and paste the App api_id and App api_hash here',
|
||||
);
|
||||
config.telegram.api_id = Number(
|
||||
await input.text('App api_id', { validate: validateNotEmpty }),
|
||||
);
|
||||
config.telegram.api_hash = await input.text('App api_hash', {
|
||||
validate: validateNotEmpty,
|
||||
});
|
||||
config.telegram.session_file = await input.text(
|
||||
'Where do you want to save the session',
|
||||
{ default: '~/.tgfs/account.session' },
|
||||
);
|
||||
|
||||
console.log('\nCreate a PRIVATE channel and paste the channel id here');
|
||||
config.telegram.private_file_channel = Number(await input.text('Channel to store the files', { validate: validateNotEmpty }));
|
||||
config.telegram.private_file_channel = Number(
|
||||
await input.text('Channel to store the files', {
|
||||
validate: validateNotEmpty,
|
||||
}),
|
||||
);
|
||||
|
||||
config.tgfs = {
|
||||
download: {
|
||||
progress: 'true',
|
||||
chunk_size_kb: 1024
|
||||
}
|
||||
}
|
||||
chunk_size_kb: 1024,
|
||||
},
|
||||
};
|
||||
|
||||
config.webdav = {
|
||||
host: '0.0.0.0',
|
||||
port: 1900,
|
||||
users: {
|
||||
user: {
|
||||
password: 'password'
|
||||
}
|
||||
}
|
||||
}
|
||||
password: 'password',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const yamlString = yaml.dump(config);
|
||||
|
||||
fs.writeFileSync(configPath, yamlString);
|
||||
return configPath;
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { ErrorCodes } from './error-codes';
|
||||
|
||||
export class TechnicalError extends Error {
|
||||
constructor(public readonly message: string, public readonly cause?: any) {
|
||||
constructor(
|
||||
public readonly message: string,
|
||||
public readonly cause?: any,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { BusinessError } from './base';
|
||||
|
||||
export class FileOrDirectoryAlreadyExistsError extends BusinessError {
|
||||
constructor(public readonly name: string, public readonly cause?: string) {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cause?: string,
|
||||
) {
|
||||
super(`${name} already exists`, 'FILE_OR_DIR_ALREADY_EXISTS', cause);
|
||||
}
|
||||
}
|
||||
|
||||
export class FileOrDirectoryDoesNotExistError extends BusinessError {
|
||||
constructor(public readonly name: string, public readonly cause?: string) {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cause?: string,
|
||||
) {
|
||||
super(
|
||||
`No such file or directory: ${name}`,
|
||||
'FILE_OR_DIR_DOES_NOT_EXIST',
|
||||
@ -17,7 +23,10 @@ export class FileOrDirectoryDoesNotExistError extends BusinessError {
|
||||
}
|
||||
|
||||
export class InvalidNameError extends BusinessError {
|
||||
constructor(public readonly name: string, public readonly cause?: string) {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cause?: string,
|
||||
) {
|
||||
super(
|
||||
`Invalid name: ${name}. Name cannot begin with -, and cannot contain /`,
|
||||
'INVALID_NAME',
|
||||
@ -27,7 +36,10 @@ export class InvalidNameError extends BusinessError {
|
||||
}
|
||||
|
||||
export class RelativePathError extends BusinessError {
|
||||
constructor(public readonly name: string, public readonly cause?: string) {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly cause?: string,
|
||||
) {
|
||||
super(
|
||||
`Relative path: ${name} is not supported. Path must start with /`,
|
||||
'RELATIVE_PATH',
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import { exit } from 'process';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
@ -13,7 +14,6 @@ import { runWebDAVServer } from './server/webdav';
|
||||
import { runSync } from './sync';
|
||||
import { Logger } from './utils/logger';
|
||||
import { sleep } from './utils/sleep';
|
||||
import fs from 'fs';
|
||||
|
||||
const { argv }: any = yargs(hideBin(process.argv))
|
||||
.option('config', {
|
||||
|
@ -2,8 +2,8 @@ import * as fs from 'fs';
|
||||
import yargs from 'yargs/yargs';
|
||||
|
||||
import { Client } from 'src/api';
|
||||
import { createDir } from 'src/api/ops/create-dir';
|
||||
import { uploadBytes } from 'src/api/ops';
|
||||
import { createDir } from 'src/api/ops/create-dir';
|
||||
import { list } from 'src/api/ops/list';
|
||||
import { removeDir } from 'src/api/ops/remove-dir';
|
||||
import { Executor } from 'src/commands/executor';
|
||||
|
@ -11,6 +11,9 @@ import { MockMessages } from './mock-messages';
|
||||
jest.mock('src/config', () => {
|
||||
return {
|
||||
config: {
|
||||
telegram: {
|
||||
private_file_channel: 'mock-private-file-channel',
|
||||
},
|
||||
tgfs: {
|
||||
download: {
|
||||
chunksize: 1024,
|
||||
@ -115,8 +118,6 @@ jest.mock('telegram', () => {
|
||||
export const createClient = async () => {
|
||||
const client = new Client(
|
||||
new TelegramClient('mock-session', 0, 'mock-api-hash', {}),
|
||||
'mock-private-channel-id',
|
||||
'mock-public-channel-id',
|
||||
);
|
||||
await client.init();
|
||||
return client;
|
||||
|
67
yarn.lock
67
yarn.lock
@ -10,13 +10,21 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.5":
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.5":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658"
|
||||
integrity sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.22.5"
|
||||
|
||||
"@babel/code-frame@^7.16.7":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3"
|
||||
integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.22.10"
|
||||
chalk "^2.4.2"
|
||||
|
||||
"@babel/compat-data@^7.22.6":
|
||||
version "7.22.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.6.tgz#15606a20341de59ba02cd2fcc5086fcbe73bf544"
|
||||
@ -52,7 +60,17 @@
|
||||
jsesc "^2.5.1"
|
||||
source-map "^0.5.0"
|
||||
|
||||
"@babel/generator@^7.17.3", "@babel/generator@^7.22.5", "@babel/generator@^7.7.2":
|
||||
"@babel/generator@^7.17.3":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722"
|
||||
integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==
|
||||
dependencies:
|
||||
"@babel/types" "^7.22.10"
|
||||
"@jridgewell/gen-mapping" "^0.3.2"
|
||||
"@jridgewell/trace-mapping" "^0.3.17"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/generator@^7.22.5", "@babel/generator@^7.7.2":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.5.tgz#1e7bf768688acfb05cf30b2369ef855e82d984f7"
|
||||
integrity sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==
|
||||
@ -157,6 +175,15 @@
|
||||
"@babel/traverse" "^7.22.6"
|
||||
"@babel/types" "^7.22.5"
|
||||
|
||||
"@babel/highlight@^7.22.10":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7"
|
||||
integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.22.5"
|
||||
chalk "^2.4.2"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/highlight@^7.22.5":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031"
|
||||
@ -166,11 +193,16 @@
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.17.3", "@babel/parser@^7.20.5", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.6":
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.5", "@babel/parser@^7.22.6":
|
||||
version "7.22.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.6.tgz#201f8b47be20c76c7c5743b9c16129760bf9a975"
|
||||
integrity sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==
|
||||
|
||||
"@babel/parser@^7.17.3", "@babel/parser@^7.20.5":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
|
||||
integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
|
||||
|
||||
"@babel/plugin-syntax-async-generators@^7.8.4":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
|
||||
@ -318,7 +350,7 @@
|
||||
"@babel/helper-validator-identifier" "^7.16.7"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.17.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3":
|
||||
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe"
|
||||
integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==
|
||||
@ -327,6 +359,15 @@
|
||||
"@babel/helper-validator-identifier" "^7.22.5"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@babel/types@^7.17.0", "@babel/types@^7.22.10":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
|
||||
integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.22.5"
|
||||
"@babel/helper-validator-identifier" "^7.22.5"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@bcoe/v8-coverage@^0.2.3":
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
@ -626,10 +667,10 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.0"
|
||||
|
||||
"@trivago/prettier-plugin-sort-imports@^4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.1.1.tgz#71c3c1ae770c3738b6fc85710714844477574ffd"
|
||||
integrity sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==
|
||||
"@trivago/prettier-plugin-sort-imports@^4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.2.0.tgz#b240366f9e2bda8e14edb18b14ea084e0ec25968"
|
||||
integrity sha512-YBepjbt+ZNBVmN3ev1amQH3lWCmHyt5qTbLCp/syXJRu/Kw2koXh44qayB1gMRxcL/gV8egmjN5xWSrYyfUtyw==
|
||||
dependencies:
|
||||
"@babel/generator" "7.17.7"
|
||||
"@babel/parser" "^7.20.5"
|
||||
@ -1186,7 +1227,7 @@ chalk@^1.0.0, chalk@^1.1.1:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
chalk@^2.0.0:
|
||||
chalk@^2.0.0, chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
@ -2809,10 +2850,10 @@ pkg-dir@^4.2.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
prettier@^2.8.8:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
prettier@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
|
||||
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
|
||||
|
||||
pretty-format@^29.0.0, pretty-format@^29.6.0:
|
||||
version "29.6.0"
|
||||
|
Loading…
Reference in New Issue
Block a user