feat(ftps): add FTP upload utility with config loading

main
uc-hoba 2 weeks ago
parent 0d59864121
commit ffdf2dc6c6
  1. 3
      bun.lock
  2. 1
      package.json
  3. 90
      src/lib/ftps.ts

@ -7,6 +7,7 @@
"dependencies": {
"@base-ui/react": "^1.5.0",
"@serwist/next": "^9.5.11",
"basic-ftp": "^6.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
@ -304,6 +305,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.35", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg=="],
"basic-ftp": ["basic-ftp@6.0.1", "", {}, "sha512-3ilxa3n4276wGQp/ImRAuz4ALdsj/2Wd3FqoZBZlajDYnByCZ0JMb4+26Rde0wGXIbM0G2HWSfr/Fi8b21KX8g=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],

@ -12,6 +12,7 @@
"dependencies": {
"@base-ui/react": "^1.5.0",
"@serwist/next": "^9.5.11",
"basic-ftp": "^6.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.4.0",

@ -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…
Cancel
Save