13.9 Static assets, file storage, upload path
บทนี้สำรวจว่าไฟล์ที่ผู้ใช้อัปโหลด, ไฟล์ PDF payslip, และ static asset อื่นถูกเก็บที่ใด, เสิร์ฟอย่างไร, และจุดที่มีความเสี่ยง
ภาพรวม file storage
┌─────────────────────────────────────────────────────────────────────┐
│ vtrc-api container │
│ │
│ /usr/src/app/storage/ ←── volume: ./api-storage (host) │
│ ├── prs/ ──── PDF payslip + tax │
│ ├── {folder}/{subfolder}/ ─── upload ทั่วไป │
│ └── ... │
│ │
│ /usr/src/app/src/storage/prs/ ─── บาง code path ใช้ (bug!) │
│ │
│ /usr/src/app/logs/ ←── volume: ./api-logs (host) │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ nginx (vtrc-web-proxy) │
│ │
│ /usr/share/nginx/html/ ←── vtrc-web build (static) │
│ /usr/share/nginx/html-app-dn/ ←── vtrc-mobile-app (static) │
│ │
└─────────────────────────────────────────────────────────────────────┘vtrc-api — การเก็บไฟล์ใน storage/
ไฟล์ที่สร้างหรือรับจากผู้ใช้ถูกเก็บใน directory storage/ ภายใต้ working directory ของ service ซึ่งก็คือ process.env.PWD ในขณะที่รัน
vtrc-api/api/src/lib/repositories/file/file.js:113-115:
var pathFolder = `${process.env.PWD}/storage/${folder}`
var fullPath = `${process.env.PWD}/storage/${folder}/${subfolder}`vtrc-api/api/src/routes.js:904:
const filePath = path.resolve(process.env.PWD, `storage/prs/${id}`);จุดสังเกต:
process.env.PWDไม่ใช่__dirname—PWDเป็น environment variable ที่ shell กำหนดตอนเริ่ม process ค่านี้อาจไม่ตรงกับ directory ของ source code ถ้าเริ่ม service จาก path อื่นstorage/prs/เป็น subdirectory หลัก สำหรับ PDF payslip — เข้าถึงได้ผ่าน REST endpoint/readfile/prs/...บาง code path ใช้
src/storage/prs/แทนstorage/prs/:vtrc-api/api/src/lib/genSlip/index.js:59vtrc-api/api/src/lib/createtax/index.js:26,145,311,598vtrc-api/api/src/lib/createslip/index.js:62vtrc-api/api/src/lib/createfailpayment/index.js:49vtrc-api/api/src/lib/controllers/withdraw/hospital.report.js:78
ความขัดแย้งของ path นี้บ่งชี้ว่าอาจมีบั๊กที่ทำให้ไฟล์ถูกเขียนไปที่ผิด หรือมีการย้าย code แล้ว path ไม่ได้ปรับตาม — ควรตรวจสอบ
vtrc-api — express.static
vtrc-api/api/src/routes.js:986-988:
app.use('/resources', express.static(process.env.PWD + '/storage/prs'));
app.use('/', express.static(process.env.PWD + '/storage'));จุดสังเกต:
/expose ทั้งstorage/ผ่าน HTTP — ทุกไฟล์ในstorage/สามารถเข้าถึงได้โดยไม่ต้องผ่าน authentication เช่นhttps://vtrcapi.redcross.or.th/<filename>เรียกไฟล์ใด ๆ ใน storage ได้โดยตรง/resourcesเป็น alias ของstorage/prs— เส้นทางสั้นสำหรับ payslip PDFไม่มี authorization check ใน static file — ใครก็ตามที่รู้ URL สามารถดาวน์โหลดไฟล์ได้
vtrc-api — /readfile/prs/ endpoint สำหรับ PDF payslip
vtrc-api/api/src/routes.js:51:
app.get(`${API_PREFIX_PATH}/readfile/prs/:year/:month/:pid/:empcode/:device/:timestamp/:hash`, async function (req, res) {URL pattern:
/api/graphql/readfile/prs/[year]/[month]/[pid]/[empcode]/[device]/[timestamp]/[hash]จุดสังเกต:
pid= identityCard และempcode— PII อยู่ใน URL path ทำให้ปรากฏใน nginx access log, browser history, referrer header (รายละเอียด Volume 9 บทที่ 1)hashเป็น signed URL token — ระบบสร้าง hash ที่มี TTL 1 ชั่วโมง ถ้า hash หมดอายุหรือไม่ตรงจะปฏิเสธ (รายละเอียด Volume 11 บทที่ 4)timestampใช้ตรวจสอบอายุของ URL — เปรียบเทียบกับเวลาปัจจุบัน
vtrc-api — volume mount สำหรับ storage
vtrc-api/docker-compose.yml:57-59,70-72,84-85,97-98:
volumes:
- ./api-storage:/usr/src/app/storage
- ./api-logs:/usr/src/app/logsจุดสังเกต:
./api-storageบน host mount เข้าทุก replica — ทำให้ไฟล์ที่เขียนใน replica 1 มองเห็นได้ใน replica 2, 3, 5- Volume เดียวกัน, หลาย writer — เสี่ยง race condition ถ้าหลาย replica เขียนไฟล์ชื่อเดียวกันพร้อมกัน (แต่ในทางปฏิบัติ PDF payslip มี path ที่ไม่ซ้ำกันตาม pid/year/month)
- Backup — directory
./api-storageอยู่บน host ต้อง backup ด้วย rsync หรือเครื่องมืออื่น — ไม่มี backup automation ที่เห็น
vtrc-api — PDF retention policy
vtrc-api/api/src/routes.js:79-82:
// หลังจากส่งไฟล์แล้วลบใน 5 วินาที
setTimeout(() => {
// delete PDF
}, 5000);จุดสังเกต:
setTimeout(deletePDF, 5000)— ลบไฟล์หลัง 5 วินาที ถ้า download ใช้เวลานานกว่านั้น ไฟล์จะถูกลบระหว่าง stream ทำให้ download ไม่สำเร็จ (รายละเอียด Volume 11 บทที่ 4)ไม่ได้ใช้
onFinishของ stream — ที่เป็นวิธีที่ถูกต้องในการลบไฟล์หลัง stream เสร็จ
nginx — static file serving
สำหรับ frontend ทั้งสองตัว, static file ถูกเสิร์ฟโดย nginx โดยตรง ไม่ผ่าน Node
vtrc-api/nginx/default.conf:250-254:
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}vtrc-api/nginx/default.conf:246-249:
location /adn/ {
root /usr/share/nginx/html-app-dn;
index index.html index.htm;
}จาก vtrc-api/docker-compose.yml:22-23:
- /home/dev/vtrc-web/build:/usr/share/nginx/html
- /home/dev/vtrc-mobile-app:/usr/share/nginx/html-app-dn/adnจุดสังเกต:
/home/dev/vtrc-web/build— path ที่ mount บ่งชีบว่าเคยเป็น dev environment ที่เก็บ build เอาไว้ (ทั้งใน prod) — ชื่อdevใน path ทำให้สับสน/home/dev/vtrc-mobile-app— มี static file ของ mobile app ที่ mount ผ่าน nginx เดียวกับเว็บ
centralize-api — file storage
centralize-api เป็น NestJS service ที่ส่วนใหญ่ทำงานกับ MSSQL — ไม่มีการจัดการ file upload ที่ซับซ้อน แต่มีการใช้ SFTP สำหรับ transfer ไฟล์:
centralize-api/src/ มี service ที่เกี่ยวข้องกับ SFTP สำหรับการ export ข้อมูล — รายละเอียดอยู่ใน Volume 5
cu-central-api — file transfer ผ่าน SCP
vtrc-api/api/.env.example:26-32:
TRANSFER_PROTOTAL="scp"
TRANSFER_HOST=""
TRANSFER_PORT="22"
TRANSFER_USERNAME=""
TRANSFER_PASSWORD=""
TRANSFER_DIRECT_DEBIT_PATH=""
TRANSFER_PAYROLL_PATH=""จุดสังเกต:
TRANSFER_PROTOTAL— typo ของ PROTOCOL ที่ใช้มานานแล้ว เปลี่ยนไม่ได้เพราะจะกระทบ code ที่อ้างถึง env var นี้SCP สำหรับ direct debit และ payroll file — ระบบสร้างไฟล์แล้ว SCP ไปยัง server ของธนาคาร หรือระบบอื่น
Credentials อยู่ใน env — username/password ของ SCP server อยู่ใน environment variable ไม่ได้ใช้ SSH key
สรุป path ที่ใช้
| Path | บนที่ไหน | บทบาท |
|---|---|---|
storage/prs/ | vtrc-api container | PDF payslip และ tax |
storage/{folder}/{subfolder}/ | vtrc-api container | upload ทั่วไป |
src/storage/prs/ | vtrc-api (บาง code path) | bug — ควรเป็น storage/prs/ |
logs/ | vtrc-api, cu-central-api | log file |
/usr/share/nginx/html | nginx vtrc-web | vtrc-web build |
/usr/share/nginx/html-app-dn | nginx vtrc-web | vtrc-mobile-app |
/usr/share/nginx/html/build | nginx backoffice | backoffice build |
./cert/ | host (mount เข้า nginx) | SSL certificate |
ปัญหาเชิงปฏิบัติการ
| ปัญหา | ที่มา | ผลกระทบ |
|---|---|---|
express.static('/', storage) ไม่มี auth | routes.js:988 | ไฟล์ใน storage เข้าถึงได้โดยไม่ต้อง login |
src/storage/prs/ vs storage/prs/ | หลาย code path | ไฟล์ถูกเขียนที่ผิด — bug ที่ยังไม่ถูกตรวจสอบ |
setTimeout(deletePDF, 5000) | routes.js:79 | download ช้าทำให้ไฟล์หายกลางทาง |
| PII ใน URL | routes.js:51 | ปรากฏใน log, history, referrer |
| Volume share หลาย replica | docker-compose | race condition ตอนเขียน |
| SCP ด้วย password ไม่ใช่ key | .env.example:29-30 | credential รั่วง่าย |
PWD แทน __dirname | หลายไฟล์ | path เปลี่ยนถ้า start จาก directory อื่น |
TRANSFER_PROTOTAL typo | .env.example:26 | ภาระในการ maintain ในอนาคต |
ข้อแนะนำเพื่อปรับปรุง
- ย้าย file storage ไป object storage — GCS หรือ S3 แทน volume ที่ mount
- เพิ่ม auth ใน static endpoint — ใช้ signed URL ที่มี expiry และ scope จำกัด
- แก้ path
src/storage/prs/ให้เป็นstorage/prs/— หรือกลับกัน แต่ต้องทำให้ตรงทุกที่ - เปลี่ยน
setTimeoutเป็นonFinish— ลบไฟล์หลัง stream เสร็จจริง - ใช้ SSH key แทน password สำหรับ SCP — หรือย้ายไปใช้ API ของธนาคารถ้าเป็นไปได้
- เพิ่ม CDN หน้า nginx — สำหรับ static file ที่ไม่ใช่ PII เพื่อลดภาระ nginx
- เปลี่ยนจาก
PWDเป็น__dirnameหรือ env var ที่กำหนดชัดเจน — ทำให้ path คาดเดาได้