Skip to content

Commit

Permalink
feat: alist webdav可以用了
Browse files Browse the repository at this point in the history
  • Loading branch information
bangbang93 committed Feb 5, 2024
1 parent e932ae7 commit ee448ce
Show file tree
Hide file tree
Showing 9 changed files with 737 additions and 46 deletions.
499 changes: 494 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"test": "true",
"build": "rm -rf dist && tsc",
"build:esbuild": "esbuild --bundle --platform=node --sourcemap --outfile=dist/openbmclapi.js src/index.ts",
"dev": "tsc-watch --onSuccess \"node dist/index.js\"",
"dev": "tsc-watch --onSuccess \"node --enable-source-maps dist/index.js\"",
"pkg": "pkg . -C Gzip",
"lint": "eslint . --ext .ts"
},
Expand Down Expand Up @@ -41,7 +41,10 @@
"pino-pretty": "^10.3.1",
"progress": "^2.0.3",
"socket.io-client": "^4.7.4",
"tail": "^2.2.6"
"tail": "^2.2.6",
"webdav": "^5.3.1",
"zod": "^3.22.4",
"zod-validation-error": "^3.0.0"
},
"devDependencies": {
"@bangbang93/eslint-config-recommended": "^0.0.3",
Expand Down
21 changes: 14 additions & 7 deletions src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function bootstrap(version: string): Promise<void> {
config.clusterSecret,
version,
)
await cluster.init()

const files = await cluster.getFileList()
logger.info(`${files.files.length} files`)
Expand All @@ -43,19 +44,25 @@ export async function bootstrap(version: string): Promise<void> {
}
}
const server = cluster.setupExpress(proto === 'https' && !config.enableNginx)
let checkFileInterval: NodeJS.Timeout
try {
await cluster.listen()
await cluster.enable()

logger.info(colors.rainbow(`done, serving ${files.files.length} files`))
if (nodeCluster.isWorker && typeof process.send === 'function') {
process.send('ready')
}

checkFileInterval = setTimeout(checkFile, ms('10m'))
} catch (e) {
logger.fatal(e)
cluster.exit(1)
}
logger.info(colors.rainbow(`done, serving ${files.files.length} files`))
if (nodeCluster.isWorker && typeof process.send === 'function') {
process.send('ready')
if (process.env.NODE_ENV === 'development') {
logger.fatal('development mode, not exiting')
} else {
cluster.exit(1)
}
}

let checkFileInterval = setTimeout(checkFile, ms('10m'))
async function checkFile(): Promise<void> {
logger.debug('refresh files')
try {
Expand Down
43 changes: 12 additions & 31 deletions src/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import got, {type Got, HTTPError} from 'got'
import {createServer, Server} from 'http'
import {createSecureServer} from 'http2'
import http2Express from 'http2-express-bridge'
import {clone, min, sum, template} from 'lodash-es'
import {clone, sum, template} from 'lodash-es'
import morgan from 'morgan'
import ms from 'ms'
import {userInfo} from 'node:os'
Expand Down Expand Up @@ -89,6 +89,10 @@ export class Cluster {
return this._port
}

public async init(): Promise<void> {
await this.storage.init?.()
}

public async getFileList(): Promise<IFileList> {
const FileListSchema = avsc.Type.forSchema({
type: 'array',
Expand All @@ -112,9 +116,7 @@ export class Cluster {
}

public async syncFiles(fileList: IFileList): Promise<void> {
const missingFiles = await Bluebird.filter(fileList.files, async (file) => {
return !await this.storage.exists(hashToFilename(file.hash))
})
const missingFiles = await this.storage.getMissingFiles(fileList.files)
if (missingFiles.length === 0) {
return
}
Expand Down Expand Up @@ -193,26 +195,10 @@ export class Cluster {
if (!await this.storage.exists(hashPath)) {
await this.downloadFile(hash)
}
const name = req.query.name as string
if (name) {
res.attachment(name)
}
res.set('x-bmclapi-hash', hash)
const path = this.storage.getAbsolutePath(hashPath)
return res.sendFile(path, {maxAge: '30d'}, (err) => {
let bytes = res.socket?.bytesWritten ?? 0
if (!err || err?.message === 'Request aborted' || err?.message === 'write EPIPE') {
const header = res.getHeader('content-length')
if (header) {
const contentLength = parseInt(header.toString(), 10)
bytes = min([bytes, contentLength]) ?? 0
}
this.counters.bytes += bytes
this.counters.hits++
} else {
if (err) return next(err)
}
})
const {bytes, hits} = await this.storage.express(hashPath, req, res, next)
this.counters.bytes += bytes
this.counters.hits += hits
} catch (err) {
if (err instanceof HTTPError) {
if (err.response.statusCode === 404) {
Expand Down Expand Up @@ -337,14 +323,9 @@ export class Cluster {
public async enable(): Promise<void> {
if (this.isEnabled) return
logger.trace('enable')
try {
await this._enable()
this.isEnabled = true
this.wantEnable = true
} catch (e) {
console.error(e)
this.exit(1)
}
await this._enable()
this.isEnabled = true
this.wantEnable = true
}

public async disable(): Promise<void> {
Expand Down
8 changes: 8 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export class Config {
public readonly disableAccessLog: boolean = false

public readonly enableNginx: boolean = false
public readonly storage: string = 'file'
public readonly storageOpts: unknown

private constructor() {
if (!process.env.CLUSTER_ID) {
Expand All @@ -35,6 +37,12 @@ export class Config {
}
this.byoc = process.env.CLUSTER_BYOC === 'true'
this.enableNginx = process.env.ENABLE_NGINX === 'true'
if (process.env.CLUSTER_STORAGE) {
this.storage = process.env.CLUSTER_STORAGE
}
if (process.env.CLUSTER_STORAGE_OPTIONS) {
this.storageOpts = JSON.parse(process.env.CLUSTER_STORAGE_OPTIONS)
}
}

public static getInstance(): Config {
Expand Down
26 changes: 26 additions & 0 deletions src/storage/alist-webdav.storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {Request, Response} from 'express'
import got from 'got'
import {join} from 'path'
import {WebdavStorage} from './webdav.storage.js'

export class AlistWebdavStorage extends WebdavStorage {
public async express(hashPath: string, req: Request, res: Response): Promise<{ bytes: number; hits: number }> {
const path = join(this.basePath, hashPath)
const url = this.client.getFileDownloadLink(path)
const resp = await got.get(url, {followRedirect: false,
responseType: 'buffer',
https: {
rejectUnauthorized: false,
}})
if (resp.statusCode === 200) {
res.send(resp.body)
return {bytes: resp.body.length, hits: 1}
}
if (resp.statusCode === 302 && resp.headers.location) {
res.status(302).location(resp.headers.location).send()
return {bytes: 0, hits: 1}
}
res.status(resp.statusCode).send(resp.body)
return {bytes: 0, hits: 0}
}
}
26 changes: 25 additions & 1 deletion src/storage/base.storage.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import type {NextFunction, Request, Response} from 'express'
import {join} from 'path'
import {cwd} from 'process'
import type {Config} from '../config.js'
import {logger} from '../logger.js'
import {AlistWebdavStorage} from './alist-webdav.storage.js'
import {FileStorage} from './file.storage.js'
import {WebdavStorage} from './webdav.storage.js'

export interface IStorage {
init?(): Promise<void>
writeFile(path: string, content: Buffer): Promise<void>

exists(path: string): Promise<boolean>

getAbsolutePath(path: string): string

getMissingFiles<T extends {path: string; hash: string}>(files: T[]): Promise<T[]>

gc(files: {path: string; hash: string; size: number}[]): Promise<void>

express(hashPath: string, req: Request, res: Response, next?: NextFunction): Promise<{ bytes: number; hits: number }>
}

export function getStorage(config: Config): IStorage {
return new FileStorage(join(cwd(), 'cache'))
let storage: IStorage
switch (config.storage) {
case 'file':
storage = new FileStorage(join(cwd(), 'cache'))
break
case 'webdav':
storage = new WebdavStorage(config.storageOpts)
break
case 'alist':
storage = new AlistWebdavStorage(config.storageOpts)
break
default:
throw new Error(`未知的存储类型${config.storage}`)
}
logger.info(`使用存储类型: ${config.storage}`)
return storage
}
35 changes: 35 additions & 0 deletions src/storage/file.storage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Bluebird from 'bluebird'
import colors from 'colors/safe.js'
import type {Request, Response} from 'express'
import fse from 'fs-extra'
import {readdir, stat, unlink} from 'fs/promises'
import {min} from 'lodash-es'
import {join, sep} from 'path'
import {logger} from '../logger.js'
import {hashToFilename} from '../util.js'
Expand All @@ -23,6 +26,12 @@ export class FileStorage implements IStorage {
return join(this.cacheDir, path)
}

public async getMissingFiles<T extends {path: string; hash: string}>(files: T[]): Promise<T[]> {
return Bluebird.filter(files, async (file) => {
return !await this.exists(hashToFilename(file.hash))
})
}

public async gc(files: {path: string; hash: string; size: number}[]): Promise<void> {
const fileSet = new Set<string>()
for (const file of files) {
Expand All @@ -48,4 +57,30 @@ export class FileStorage implements IStorage {
}
} while (queue.length !== 0)
}

public async express(hashPath: string, req: Request, res: Response): Promise<{ bytes: number; hits: number }> {
const name = req.query.name as string
if (name) {
res.attachment(name)
}
const path = this.getAbsolutePath(hashPath)
return new Promise((resolve, reject) => {
res.sendFile(path, {maxAge: '30d'}, (err) => {
let bytes = res.socket?.bytesWritten ?? 0
if (!err || err?.message === 'Request aborted' || err?.message === 'write EPIPE') {
const header = res.getHeader('content-length')
if (header) {
const contentLength = parseInt(header.toString(), 10)
bytes = min([bytes, contentLength]) ?? 0
}
resolve({bytes, hits: 1})
} else {
if (err) {
return reject(err)
}
resolve({bytes: 0, hits: 0})
}
})
})
}
}
Loading

0 comments on commit ee448ce

Please sign in to comment.