Commit 21d6eb88 authored by nanahira's avatar nanahira

add ott

parent 8def6514
Pipeline #17461 failed with stages
in 1 minute and 1 second
This diff is collapsed.
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.196.0",
"@aws-sdk/lib-storage": "^3.196.0",
"@nestjs/axios": "^0.0.1", "@nestjs/axios": "^0.0.1",
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.1", "@nestjs/config": "^1.0.1",
...@@ -32,6 +34,7 @@ ...@@ -32,6 +34,7 @@
"form-data": "^3.0.1", "form-data": "^3.0.1",
"hasha": "^5.2.2", "hasha": "^5.2.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.29.4",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
......
...@@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config'; ...@@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
import { HttpModule } from '@nestjs/axios'; import { HttpModule } from '@nestjs/axios';
import { UtilityModule } from './utility/utility.module'; import { UtilityModule } from './utility/utility.module';
import { JsdModule } from './jsd/jsd.module'; import { JsdModule } from './jsd/jsd.module';
import { OneTwoThreeModule } from './one-two-three/one-two-three.module';
@Module({ @Module({
imports: [ imports: [
...@@ -13,6 +14,7 @@ import { JsdModule } from './jsd/jsd.module'; ...@@ -13,6 +14,7 @@ import { JsdModule } from './jsd/jsd.module';
}, },
UtilityModule, UtilityModule,
JsdModule, JsdModule,
OneTwoThreeModule,
], ],
}) })
export class AppModule {} export class AppModule {}
...@@ -5,4 +5,12 @@ export class MiddlewareInfoDto { ...@@ -5,4 +5,12 @@ export class MiddlewareInfoDto {
identifier: string; identifier: string;
@ApiProperty({ description: '最大文件大小' }) @ApiProperty({ description: '最大文件大小' })
maxSize?: number; maxSize?: number;
@ApiProperty({ description: '是否需要回调' })
callback?: boolean;
constructor(identifier: string, maxSize?: number, callback?: boolean) {
this.identifier = identifier;
this.maxSize = maxSize;
this.callback = callback;
}
} }
...@@ -11,6 +11,12 @@ export class PostUrlDto { ...@@ -11,6 +11,12 @@ export class PostUrlDto {
@ApiProperty({ description: '可能的文件大小' }) @ApiProperty({ description: '可能的文件大小' })
size?: number; size?: number;
@ApiProperty({ description: '文件名' })
path?: string;
@ApiProperty({ description: '文件 sha256' })
hash?: string;
@ApiProperty({ description: '自定义后缀' }) @ApiProperty({ description: '自定义后缀' })
customSuffix?: string; customSuffix?: string;
...@@ -18,9 +24,9 @@ export class PostUrlDto { ...@@ -18,9 +24,9 @@ export class PostUrlDto {
customMime?: string; customMime?: string;
getFilename() { getFilename() {
const urlHash = hasha(this.url, { algorithm: 'sha256' }); const urlHash = this.path || hasha(this.url, { algorithm: 'sha256' });
if (this.customSuffix) { if (this.customSuffix) {
return `${this.url}.${this.customSuffix}`; return `${urlHash}.${this.customSuffix}`;
} }
const lastPattern = this.url.split('/').pop(); const lastPattern = this.url.split('/').pop();
if (!lastPattern.length) { if (!lastPattern.length) {
......
...@@ -30,10 +30,7 @@ export class JsdService extends MiddlewareService { ...@@ -30,10 +30,7 @@ export class JsdService extends MiddlewareService {
'JSDelivr is not configured.', 'JSDelivr is not configured.',
).toException(); ).toException();
} }
const info = new MiddlewareInfoDto(); return new MiddlewareInfoDto(this.jsdIdentifier, 50 * 1024 ** 2);
info.identifier = this.jsdIdentifier;
info.maxSize = 50 * 1024 * 1024;
return info;
} }
async upload(urlDto: PostUrlDto) { async upload(urlDto: PostUrlDto) {
......
import { Test, TestingModule } from '@nestjs/testing';
import { OneTwoThreeController } from './one-two-three.controller';
import { OneTwoThreeService } from './one-two-three.service';
describe('OneTwoThreeController', () => {
let controller: OneTwoThreeController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [OneTwoThreeController],
providers: [OneTwoThreeService],
}).compile();
controller = module.get<OneTwoThreeController>(OneTwoThreeController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import { Controller } from '@nestjs/common';
import { OneTwoThreeService } from './one-two-three.service';
import { MiddlewareController } from '../abstract/controller';
@Controller('ott')
export class OneTwoThreeController extends MiddlewareController {
constructor(s: OneTwoThreeService) {
super(s);
}
}
import { Module } from '@nestjs/common';
import { OneTwoThreeService } from './one-two-three.service';
import { OneTwoThreeController } from './one-two-three.controller';
@Module({
controllers: [OneTwoThreeController],
providers: [OneTwoThreeService]
})
export class OneTwoThreeModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { OneTwoThreeService } from './one-two-three.service';
describe('OneTwoThreeService', () => {
let service: OneTwoThreeService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [OneTwoThreeService],
}).compile();
service = module.get<OneTwoThreeService>(OneTwoThreeService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { MiddlewareService } from '../abstract/service';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import { UtilityService } from '../utility/utility/utility.service';
import { BlankReturnMessageDto } from '../dto/ReturnMessage.dto';
import { MiddlewareInfoDto } from '../dto/MiddlewareInfo.dto';
import { lastValueFrom } from 'rxjs';
import moment, { Moment } from 'moment';
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
interface OTTResponse<T> {
code: number;
message: string;
data: T;
}
interface LoginResultData {
expire: string;
login_type: number;
refresh_token_expire_time: number;
token: string;
}
interface UploadCompleteReq {
Info?: ServerFileInfo;
}
interface UploadReq extends UploadCompleteReq {
AccessKeyId: string;
SecretAccessKey: string;
SessionToken: string;
Expiration: string;
Key: string;
Bucket: string;
FileId: number;
Reuse: boolean;
UploadId: string;
DownloadUrl: string;
}
interface FileInfo {
etag: string;
fileId: number;
fileName: string;
s3keyFlag: string;
size: number;
}
interface ServerFileInfo {
FileId: number;
FileName: string;
Type: number;
Size: number;
ContentType: string;
S3KeyFlag: string;
CreateAt: string;
UpdateAt: string;
Hidden: boolean;
Etag: string;
Status: number;
ParentFileId: number;
Category: number;
PunishFlag: number;
ParentName: string;
DownloadUrl: string;
}
function adaptFileInfo(info: ServerFileInfo): FileInfo {
return {
etag: info.Etag,
fileId: info.FileId,
fileName: info.FileName,
s3keyFlag: info.S3KeyFlag,
size: info.Size,
};
}
@Injectable()
export class OneTwoThreeService extends MiddlewareService {
username = this.config.get('OTT_USERNAME');
password = this.config.get('OTT_PASSWORD');
directory = parseInt(this.config.get('OTT_DIRECTORY'));
identifier =
this.config.get('OTT_IDENTIFIER') ||
`ott-${this.username || 'undefined'}-${this.directory}`;
constructor(
private config: ConfigService,
http: HttpService,
private utility: UtilityService,
) {
super('one-two-three', http);
}
token: string;
tokenExpireTime: Moment;
private async login(relogin = false) {
if (
!relogin &&
this.token &&
this.tokenExpireTime &&
this.tokenExpireTime.isAfter(moment())
) {
return this.token;
}
const { data } = await lastValueFrom(
this.http.post<OTTResponse<LoginResultData>>(
'https://www.123pan.com/api/user/sign_in',
{ passport: this.username, password: this.password },
{ responseType: 'json' },
),
);
if (data.code < 300) {
this.token = data.data.token;
this.tokenExpireTime = moment.unix(data.data.refresh_token_expire_time);
return data.data.token;
}
this.error(`Login failed: ${data.message}`);
throw new BlankReturnMessageDto(data.code, data.message).toException();
}
async info() {
if (!this.username) {
throw new BlankReturnMessageDto(
404,
'ott is not configured.',
).toException();
}
return new MiddlewareInfoDto(this.identifier, 100 * 1024 ** 3, true);
}
async upload(info) {
try {
const fileName = info.getFilename();
const uploadReq = await lastValueFrom(
this.http.post<OTTResponse<UploadReq>>(
'https://www.123pan.com/api/file/upload_request',
{
/// https://github.com/alist-org/alist/blob/main/drivers/123/driver.go#L222
driveId: 0,
duplicate: 2,
etag: info.hash?.slice(0, 32),
parentFileId: this.directory,
fileName: fileName,
size: info.size,
type: 0,
},
{
responseType: 'json',
headers: {
Authorization: `Bearer ${await this.login()}`,
},
},
),
);
if (uploadReq.data.code > 300) {
throw new Error(`Upload request failed: ${uploadReq.data.message}`);
}
if (!uploadReq.data.data.AccessKeyId) {
// already uploaded
// return JSON.stringify(adaptFileInfo(uploadReq.data.data.Info));
return await this.getDownloadUrl(
adaptFileInfo(uploadReq.data.data.Info),
);
}
const s3 = new S3Client({
credentials: {
accessKeyId: uploadReq.data.data.AccessKeyId,
secretAccessKey: uploadReq.data.data.SecretAccessKey,
sessionToken: uploadReq.data.data.SessionToken,
},
region: '123pan',
endpoint: 'https://file.123pan.com',
forcePathStyle: true,
});
const streamInfo = await this.utility.getStreamFromUrl(info.url);
const upload = new Upload({
client: s3,
params: {
Bucket: uploadReq.data.data.Bucket,
Key: uploadReq.data.data.Key,
Body: streamInfo.data,
ContentType: info.customMime || streamInfo.headers['content-type'],
ContentLength: info.size,
},
});
await upload.done();
const completeReq = await lastValueFrom(
this.http.post<OTTResponse<UploadCompleteReq>>(
'https://www.123pan.com/api/file/upload_complete',
{ fileId: uploadReq.data.data.FileId },
{
responseType: 'json',
headers: {
Authorization: `Bearer ${await this.login()}`,
},
},
),
);
return await this.getDownloadUrl(adaptFileInfo(uploadReq.data.data.Info));
} catch (e) {
this.error(`Upload failed: ${e.message}`);
throw new BlankReturnMessageDto(500, e.message).toException();
}
}
async getDownloadUrl(fileInfo: FileInfo) {
const downloadReq = await lastValueFrom(
this.http.post<OTTResponse<UploadReq>>(
'https://www.123pan.com/api/file/download_info',
{ ...fileInfo, driveId: 0, type: 0 },
{
responseType: 'json',
headers: {
Authorization: `Bearer ${await this.login()}`,
},
},
),
);
if (downloadReq.data.code > 300) {
throw new Error(`Download request failed: ${downloadReq.data.message}`);
}
return downloadReq.data.data.DownloadUrl;
}
async parse302(url: string) {
const tmpLinkReq = await lastValueFrom(
this.http.head(url, {
maxRedirects: 0,
validateStatus: (c) => c === 302 || c === 301 || c === 200,
}),
);
if (tmpLinkReq.status !== 200) {
return tmpLinkReq.headers.location;
}
return url;
}
async download(info) {
try {
return await this.parse302(info.url);
} catch (e) {
this.error(`Download failed: ${e.message}`);
throw new BlankReturnMessageDto(500, e.message).toException();
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment