parent
0d59864121
commit
ffdf2dc6c6
3 changed files with 94 additions and 0 deletions
@ -0,0 +1,90 @@ |
||||
import { randomUUID } from 'node:crypto'; |
||||
import { createReadStream } from 'node:fs'; |
||||
import { extname } from 'node:path'; |
||||
import { Client } from 'basic-ftp'; |
||||
|
||||
export interface FtpConfig { |
||||
host: string; |
||||
port: number; |
||||
user: string; |
||||
password: string; |
||||
/** Remote directory the files are uploaded into. */ |
||||
remoteDir: string; |
||||
/** Public base URL that serves files from `remoteDir`. */ |
||||
publicBaseUrl: string; |
||||
} |
||||
|
||||
/** |
||||
* Build the FTP config from environment variables. |
||||
* |
||||
* Required: FTP_HOST / FTP_USER / FTP_PASSWORD |
||||
* Optional: FTP_PORT (990) / FTP_REMOTE_DIR (/) / FTP_PUBLIC_BASE_URL |
||||
*/ |
||||
export function loadFtpConfig(): FtpConfig { |
||||
const { |
||||
FTP_HOST, |
||||
FTP_PORT, |
||||
FTP_USER, |
||||
FTP_PASSWORD, |
||||
FTP_REMOTE_DIR, |
||||
FTP_PUBLIC_BASE_URL, |
||||
} = process.env; |
||||
|
||||
if (!FTP_HOST || !FTP_USER || !FTP_PASSWORD) { |
||||
throw new Error( |
||||
'Missing required FTP env vars (FTP_HOST / FTP_USER / FTP_PASSWORD)', |
||||
); |
||||
} |
||||
|
||||
return { |
||||
host: FTP_HOST, |
||||
port: FTP_PORT ? Number(FTP_PORT) : 990, |
||||
user: FTP_USER, |
||||
password: FTP_PASSWORD, |
||||
remoteDir: FTP_REMOTE_DIR ?? '/', |
||||
publicBaseUrl: FTP_PUBLIC_BASE_URL ?? 'https://dl.giant.com.tw/MQR', |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Uploads a local file to the FTP server under a random `<uuid><ext>` name and |
||||
* returns the public URL of the uploaded file. |
||||
* |
||||
* A random UUID name avoids collisions and, since the server rejects |
||||
* overwriting existing files, guarantees every upload is a fresh create. |
||||
* |
||||
* NOTE: must be run on the Node.js runtime (e.g. `tsx`), not Bun — Bun does not |
||||
* support the TLS session resumption this FTPS server requires on the data |
||||
* connection. |
||||
* |
||||
* @param localPath Path to the local file to upload (its extension is kept). |
||||
* @param config Optional FTP config; defaults to `loadFtpConfig()`. |
||||
* @returns The public URL of the uploaded file. |
||||
*/ |
||||
export async function uploadFile( |
||||
localPath: string, |
||||
config: FtpConfig = loadFtpConfig(), |
||||
): Promise<string> { |
||||
const ext = extname(localPath).toLowerCase(); |
||||
const remoteName = `${randomUUID()}${ext}`; |
||||
const remoteRoot = config.remoteDir.replace(/\/$/, ''); |
||||
const remotePath = `${remoteRoot}/${remoteName}`; |
||||
|
||||
const client = new Client(30_000); |
||||
try { |
||||
await client.access({ |
||||
host: config.host, |
||||
port: config.port, |
||||
user: config.user, |
||||
password: config.password, |
||||
secure: 'implicit', |
||||
secureOptions: { rejectUnauthorized: false }, |
||||
}); |
||||
|
||||
await client.uploadFrom(createReadStream(localPath), remotePath); |
||||
|
||||
return `${config.publicBaseUrl.replace(/\/$/, '')}/${remoteName}`; |
||||
} finally { |
||||
client.close(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue