LBK Script Manager

Máy chủ phân phối & bộ công cụ tự động hóa toàn diện cho VPS aaPanel / OpenLiteSpeed

Sẵn sàng (32/32 files)
curl -Lso- script.lbk.vn | bash
Nếu cần sudo: curl -Lso- script.lbk.vn | sudo bash
Mở menu quản trị: lbk hoặc LBK
Cập nhật bản mới: /root/lbk/update.sh
Gỡ cài đặt sạch: bash /root/lbk/uninstall.sh

INFO Thông tin VPS & Cấu hình

Tổng quan phần cứng CPU, RAM, Swap, Disk, Port SSH, OS, OpenLiteSpeed, MySQL, PHP và toàn bộ thông số cấu hình LBK trên 1 màn hình trực quan.

GDRIVE Google Drive (Rclone)

Tự động cài đặt Rclone, kết nối toàn bộ Drive hoặc theo Root Folder ID cố định. Kiểm tra token tức thì, quản lý danh sách remote linh hoạt.

AUTH Phân quyền OLS & Pure-FTPd

Phân quyền 755/644, user riêng biệt cô lập từng domain, khóa bất biến .user.ini. Đồng bộ tài khoản FTP thẳng vào SQLite aaPanel default.db.

BACKUP Sao lưu Web & DB (.tar.gz)

Đóng gói hợp nhất mã nguồn, SQL dump và metadata vào 1 file duy nhất trên Google Drive. Nhận diện DB thông minh qua 4 lớp chính xác.

CRON Đặt lịch giờ Việt Nam (GMT+7)

Nhập giờ phút theo giờ Việt Nam, script tự động quy đổi lệch múi giờ VPS để ghi crontab chuẩn xác 100%, tự dọn file backup cũ trên Drive.

RESTORE Khôi phục an toàn (Safe Fallback)

Kiểm tra tính toàn vẹn file và DB trước khi xóa web cũ. Tự động backup dự phòng DB hiện tại vào /tmp đề phòng sự cố, tự tạo vhost aaPanel.

FAIL2BAN Bảo mật chống Brute-Force

4 Jail bảo vệ: sshd, wordpress-auth (chặn wp-login/xmlrpc), http-dos (Layer 7 DoS), wordpress-spam. Quét log OLS, Nginx, Apache và bảo toàn IP ban.

SECURITY Bảo mật OLS & Anti-DDoS

Tối ưu OLS Connection Limiting, Bandwidth Throttling, khóa truy cập xmlrpc.php WordPress chống amplification attack, cấm chạy PHP trong uploads.

SHEET Đồng bộ danh sách Web

Quét toàn bộ website: CMS, Plugin/Cache, Dung lượng Web/DB, Hạn SSL, Link Backup và tự động đồng bộ lên Google Sheet qua Webhook.

WATCH Giám sát Realtime (Watcher)

Theo dõi thư mục /www/wwwroot realtime bằng background worker, tự động trigger đồng bộ danh sách lên Sheet ngay khi thêm/xóa web hoặc domain.

FIND Tìm kiếm file lớn & Log tràn

Quét nhanh file >100MB, file backup cũ (.tar.gz, .zip, .sql), file log khổng lồ phình to làm tràn ổ cứng, hỗ trợ đọc và truncate dọn sạch an toàn.

SWAP Quản lý & Tối ưu bộ nhớ ảo

Tạo file Swap tự động (1GB - 8GB), tinh chỉnh swappiness (10-20) tối ưu cho web server, chống đứng VPS và chống MySQL bị OOM Killer tắt đột ngột.

IMAGE Tối ưu & Nén ảnh website

Tự cài jpegoptim/optipng, quét và nén ảnh trong uploads WordPress giảm 30-70% dung lượng mà không vỡ hình, xử lý đa luồng theo CPU core.

LIFECYCLE Cập nhật & Gỡ cài đặt sạch

Script update.sh kéo mã nguồn mới nhất từ script.lbk.vn giữ nguyên cấu hình; uninstall.sh dọn dẹp sạch sẽ mọi dấu vết khỏi VPS khi cần.

Mã nguồn Google Apps Script Webhook

Dán mã này vào Google Sheet của bạn để nhận dữ liệu đồng bộ từ các VPS.

1

Mở Google Sheet & Apps Script

Tạo một Google Sheet mới (hoặc mở sheet có sẵn), vào menu: Tiện ích mở rộng (Extensions) -> Apps Script.

2

Dán mã nguồn & Lưu lại

Xóa hết code mặc định trong file Code.gs, dán toàn bộ đoạn mã bên dưới vào và bấm Lưu (Ctrl+S).

3

Triển khai dưới dạng Web App

Bấm Triển khai (Deploy) -> Triển khai mới (New deployment) -> Chọn loại Ứng dụng web (Web app).
Mục Người có quyền truy cập (Who has access): Chọn Bất kỳ ai (Anyone) -> Bấm Triển khai.

4

Lấy Webhook URL cấu hình vào VPS

Copy đường link Web App vừa nhận được (đuôi /exec), trên VPS mở lbk -> Chọn mục 8. Đồng bộ danh sách web lên Google Sheet -> Dán URL vào.

/** * ============================================================================ * GOOGLE APPS SCRIPT - DONG BO DANH SACH WEB CHO LBK SCRIPT (aaPanel + OLS) * ============================================================================ * * HUONG DAN CAI DAT 1 LAN DUY NHAT: * 1. Tren Google Sheets, tao 1 trang tinh moi (hoac mo trang tinh hien co). * 2. Vao menu: Tien ich mo rong (Extensions) -> Apps Script. * 3. Xoa toan bo ma cu trong trinh bien tap, dan toan bo noi dung file nay vao. * 4. (Khuyen nghi) Dat SECRET_KEY o dong 26 de tang tinh bao mat chong spam. * 5. Bam nut "Luu" (Save / bieu tuong dia mem). * 6. Bam nut "Trien khai" (Deploy) -> "Trien khai moi" (New deployment). * - Loai (Select type): Chon "Ung dung web" (Web app). * - Mo ta (Description): LBK Sync Webhook. * - Thuc thi duoi quyen (Execute as): "Toi" (Me / email Google cua ban). * - Nguoi co quyen truy cap (Who has access): "Bat ky ai" (Anyone). * 7. Bam "Trien khai" (Deploy), neu Google yeu cau cap quyen thi bam "Uy quyen truy cap". * 8. Sao chep "URL ung dung web" (Web app URL) co duoi /exec dang: * https://script.google.com/macros/s/.../exec * 9. Dan URL nay vao menu LBK Script tren VPS de bat dau dong bo! * ============================================================================ */ // ============================================================================ // CAU HINH CHUNG // ============================================================================ // Dat SECRET_KEY neu muon bat buoc xac thuc (de trong "" neu khong can) var SECRET_KEY = ""; // Ten Tab (Sheet) luu danh sach website var SHEET_NAME = "Danh sach Web"; // Tieu de cac cot (8 cot) // Col 1: IP VPS (gom IP + nha cung cap + so web + dung luong disk) // Col 2: Domain // Col 3: Ma nguon // Col 4: Plugin/Cache (VD: LiteSpeed / HIT, wp-rocket / MISS, -) // Col 5: Dung luong Web/DB // Col 6: Han SSL // Col 7: Backup moi nhat // Col 8: Cap nhat luc var HEADERS = [ "IP VPS", "Domain", "Ma nguon", "Plugin/Cache", "Dung luong Web/DB", "Han SSL", "Backup moi nhat", "Cap nhat luc" ]; // Vi tri cot Plugin/Cache trong sheet moi (1-based) var CACHE_COL = 4; // ============================================================================ // CAU HINH GIAO DIEN & MAU SAC (THEME) // ============================================================================ // Chieu rong tung cot (theo thu tu tu cot 1 den cot 8) var COLUMN_WIDTHS = [ 200, // Col 1: IP VPS 220, // Col 2: Domain 200, // Col 3: Ma nguon 160, // Col 4: Plugin/Cache 150, // Col 5: Dung luong Web/DB 120, // Col 6: Han SSL 250, // Col 7: Backup moi nhat 150 // Col 8: Cap nhat luc ]; // Chieu cao hang Header (dong 1) var HEADER_ROW_HEIGHT = 35; // Mau Header (dong 1) var COLOR_HEADER_BG = "#1b73e8"; // Xanh duong var COLOR_HEADER_FG = "#ffffff"; // Chu trang // Mau nen cac dong du lieu so le nhau var COLOR_ROW_EVEN = "#ffffff"; // Dong chan: Trang var COLOR_ROW_ODD = "#e0f7fa"; // Dong le: Xanh ngoc nhat // Mau vien (Border) bao quanh bang va ke o var COLOR_BORDER = "#dadce0"; // Mau trang thai cot Plugin/Cache (Col 4) - mau nen dung chung so le theo dong var COLOR_CACHE_HIT_FG = "#137333"; // HIT: Chu xanh la dam var COLOR_CACHE_ERR_FG = "#c5221f"; // ERR: Chu do dam var COLOR_CACHE_MISS_FG = "#856404"; // MISS: Chu vang dam // Mau trang thai cot Han SSL (Col 6) var COLOR_SSL_EXP_BG = "#fce8e6"; // Het han / Khong co SSL: Nen do nhat var COLOR_SSL_EXP_FG = "#c5221f"; // Chu do dam var COLOR_SSL_WARN_BG = "#fef9c3"; // Sap het han (< 10 ngay): Nen vang nhat var COLOR_SSL_WARN_FG = "#856404"; // Chu vang dam // Mau chu mac dinh & phan cach var COLOR_TEXT_NORMAL = "#202124"; // Chu den xam mac dinh var COLOR_TEXT_MUTED = "#5f6368"; // Chu xam ghi chu / placeholder "-" // Mau chu RichText cot Dung luong Web/DB (Col 5) var COLOR_WEB_SIZE = "#137333"; // Web size: Xanh la var COLOR_DB_SIZE = "#e65100"; // Database size: Cam /** * Helper tra ve JSON response */ function jsonResponse(obj) { return ContentService.createTextOutput(JSON.stringify(obj)) .setMimeType(ContentService.MimeType.JSON); } /** * Loc chong Formula Injection trong Google Sheet (=, +, -, @) */ function sanitizeForSheet(val, maxLen) { var str = String(val || "").trim().substring(0, maxLen); // Ngoai le: "-" dung lam placeholder "khong co DB" -> giu nguyen. // Mot minh "-" khong phai formula trong Sheets nen an toan, // neu them "'" se bi hien thi thua thanh "'-" khi ghep "136K / -". if (str === "-") { return str; } if (/^[=+\-@]/.test(str)) { str = "'" + str; } return str; } /** * Chuan hoa gia tri Web/DB cu bi nhiem dau "'" tu ban cu. * VD: "136K / '-" -> "136K / -" ; "'-" -> "-" * Chi don dung artifact nay, khong bo prefix "'" cua cac gia tri * chong formula-injection hop le khac (=, +, @...). */ function normalizeLegacyWebDb(val) { var str = String(val || ""); if (str.indexOf("'") === -1) { return str; } str = str.replace(/ \/ '-/g, " / -"); if (str.substring(0, 2) === "'-") { str = str.substring(1); } return str; } /** * Tao RichTextValue cho cot Dung luong Web/DB: * - Web size: mau xanh green (#137333), bold * - Phan ngan cach " / ": mau xam (#5f6368), normal * - Database size: mau cam (#e65100), bold * - Placeholder "-": mau xam (#5f6368), normal */ function buildWebDbRichText(val, styleWeb, styleDb, styleSep) { var str = String(val || "").trim(); if (!str || str === "-") { var bEmpty = SpreadsheetApp.newRichTextValue().setText(str || "-"); if (styleSep) { try { bEmpty.setTextStyle(0, (str || "-").length, styleSep); } catch (e) {} } return bEmpty.build(); } // Ho tro ca " / " lan "/" (phong truong hop sua tay lam mat khoang trang) var sep = " / "; var sepLen = 3; var slashIdx = str.indexOf(sep); if (slashIdx === -1 && str.indexOf("/") !== -1) { sep = "/"; sepLen = 1; slashIdx = str.indexOf(sep); } var builder = SpreadsheetApp.newRichTextValue().setText(str); if (slashIdx === -1) { // Chi co 1 gia tri (web hoac db) if (styleWeb) { try { builder.setTextStyle(0, str.length, styleWeb); } catch (e) {} } return builder.build(); } var webPart = str.substring(0, slashIdx); var dbStart = slashIdx + sepLen; var dbPart = str.substring(dbStart); try { // Web: xanh green neu co dung luong, xam neu "-" if (slashIdx > 0) { if (webPart === "-") { if (styleSep) builder.setTextStyle(0, slashIdx, styleSep); } else { if (styleWeb) builder.setTextStyle(0, slashIdx, styleWeb); } } // Ngan cach " / " hoac "/": xam if (sepLen > 0 && slashIdx >= 0) { if (styleSep) builder.setTextStyle(slashIdx, dbStart, styleSep); } // DB: mau cam neu co dung luong, xam neu "-" if (dbStart < str.length) { if (dbPart === "-") { if (styleSep) builder.setTextStyle(dbStart, str.length, styleSep); } else { if (styleDb) builder.setTextStyle(dbStart, str.length, styleDb); } } } catch (eStyle) { console.warn("[CANH BAO] Loi setTextStyle cot Web/DB: " + eStyle.toString()); } return builder.build(); } /** * Xu ly khi VPS gui du lieu qua POST (curl) */ function doPost(e) { if (!e || !e.postData || !e.postData.contents) { return jsonResponse({ status: "error", message: "Empty payload" }); } var data; try { data = JSON.parse(e.postData.contents); } catch (parseErr) { return jsonResponse({ status: "error", message: "Invalid JSON: " + parseErr.message }); } if (SECRET_KEY !== "" && String(data.secret || "") !== SECRET_KEY) { return jsonResponse({ status: "error", message: "Unauthorized: Invalid secret key" }); } var vpsIp = String(data.vps_ip || "").trim(); if (!/^[0-9]{1,3}(\.[0-9]{1,3}){3}$/.test(vpsIp)) { return jsonResponse({ status: "error", message: "Invalid vps_ip (expect IPv4)" }); } var _oct = vpsIp.split("."); for (var _k = 0; _k < _oct.length; _k++) { if (Number(_oct[_k]) > 255) { return jsonResponse({ status: "error", message: "Invalid vps_ip (octet > 255)" }); } } vpsIp = sanitizeForSheet(vpsIp, 45); var sites = Array.isArray(data.sites) ? data.sites : []; var isClearRequest = (data.clear === true || data.action === "clear"); if (sites.length === 0 && !isClearRequest) { return jsonResponse({ status: "skipped", message: "Danh sach sites rong, bo qua de tranh xoa nham du lieu tren Sheet" }); } if (sites.length > 500) { return jsonResponse({ status: "error", message: "Too many sites, max 500 allowed per request" }); } var provider = sanitizeForSheet(data.provider || "", 100) || "Chua xac dinh"; var diskUsage = sanitizeForSheet(data.disk_usage || "", 50); var timestamp = Utilities.formatDate(new Date(), "GMT+7", "HH:mm dd-MM-yyyy"); // Tao chuoi hien thi o cot IP: // 103.221.220.42 - AZDIGI // (11 Web / 41G / 51G) var ipLine1 = vpsIp + " - " + provider; var ipLine2 = ""; if (sites.length > 0) { ipLine2 = "(" + sites.length + " Web" + (diskUsage ? " / " + diskUsage : "") + ")"; } var ipDisplay = ipLine2 ? (ipLine1 + "\n" + ipLine2) : ipLine1; var lock = LockService.getScriptLock(); try { lock.waitLock(30000); } catch (err) { return jsonResponse({ status: "error", message: "Server busy, could not obtain lock within 30 seconds" }); } try { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName(SHEET_NAME); if (!sheet) { sheet = ss.insertSheet(SHEET_NAME); } var lastRow = sheet.getLastRow(); var lastCol = sheet.getLastColumn(); var keptGroups = {}; var groupOrder = []; // Nhan dien layout: 8 cot (moi), 9 cot (co bang thong + cache), 8 cot (cu - co bang thong chua co cache), hoac 7 cot (legacy) var L = "v8_nobw"; if (lastRow > 0) { var headerVals = (lastCol >= 4) ? sheet.getRange(1, 1, 1, Math.min(lastCol, 10)).getValues()[0] : []; function hd(c) { return String(headerVals[c - 1] || "").trim(); } var h4 = hd(4), h5 = hd(5), h6 = hd(6), h7 = hd(7); if (h4 === "Plugin/Cache" && h6 === "Han SSL") { L = "v8_nobw"; } else if (h4 === "Plugin/Cache" && (h6 === "Bang thong (Thang)" || h7 === "Han SSL")) { L = "v9"; } else if (h4 === "Dung luong Web/DB" && (h5 === "Bang thong (Thang)" || h6 === "Han SSL")) { L = "v8_bw"; } else if (h4 === "Dung luong Web/DB" && h5 === "Han SSL") { L = "v7"; } else if (lastCol >= 9) { L = "v9"; } else if (lastCol === 8) { L = "v8_nobw"; } else { L = "v7"; } } // Doc du lieu cu, giu lai cac VPS khac if (lastRow > 1) { var readCols = Math.max(lastCol, HEADERS.length); var allRows = sheet.getRange(2, 1, lastRow - 1, readCols).getValues(); var backupColOld = (L === "v9") ? 8 : ((L === "v8_nobw" || L === "v8_bw") ? 7 : 6); var allBackupRich = sheet.getRange(2, backupColOld, lastRow - 1, 1).getRichTextValues(); var curIp = ""; var curIpDisplay = ""; for (var i = 0; i < allRows.length; i++) { var row = allRows[i]; var rowRaw = String(row[0] || "").trim(); if (rowRaw !== "") { // IP la dong dau cua chuoi (truoc "\n") curIp = rowRaw.split("\n")[0].trim(); // Tach IP khoi "IP - Provider" curIp = curIp.split(" - ")[0].trim(); curIpDisplay = rowRaw; // Luu thu tu vi tri cua tat ca cac VPS trong sheet (ke ca vpsIp dang cap nhat) if (curIp !== "" && groupOrder.indexOf(curIp) === -1) { groupOrder.push(curIp); } } if (curIp !== "" && curIp !== vpsIp) { if (!keptGroups[curIp]) { keptGroups[curIp] = { ipDisplay: curIpDisplay, items: [] }; } // items: [domain, cms, cache, webdb, ssl, backup_obj, timestamp] // Lay RichTextValue cua o backup de GIU NGUYEN toan bo link URL cho cac VPS khac var curRich = (allBackupRich && allBackupRich[i]) ? allBackupRich[i][0] : null; var oldCache, oldWebdb, oldSsl, oldTs; if (L === "v8_nobw") { oldCache = sanitizeForSheet(row[3], 150); oldWebdb = row[4]; oldSsl = row[5]; oldTs = row[7]; } else if (L === "v9") { oldCache = sanitizeForSheet(row[3], 150); oldWebdb = row[4]; // row[5] la Bang thong cu -> bo oldSsl = row[6]; oldTs = row[8]; } else if (L === "v8_bw") { oldCache = "-"; oldWebdb = row[3]; // row[4] la Bang thong cu -> bo oldSsl = row[5]; oldTs = row[7]; } else { // v7 oldCache = "-"; oldWebdb = row[3]; oldSsl = row[4]; oldTs = row[6]; } keptGroups[curIp].items.push([ row[1], // domain row[2], // cms oldCache, normalizeLegacyWebDb(oldWebdb), oldSsl, curRich, // backup RichTextValue (co san link) oldTs ]); } } } // Khoi tao hoac nang cap Header len 8 cot (sau khi da doc xong du lieu cu vao RAM) initHeaderIfNeeded(sheet); // Them du lieu moi cua VPS nay if (sites.length > 0) { keptGroups[vpsIp] = { ipDisplay: ipDisplay, items: [] }; for (var s = 0; s < sites.length; s++) { var item = sites[s] || {}; var webSize = sanitizeForSheet(item.size || "", 50); var dbSize = sanitizeForSheet(item.db_size || "", 50) || "-"; var webdb = webSize ? (webSize + " / " + dbSize) : (dbSize !== "-" ? ("- / " + dbSize) : "-"); var cache = sanitizeForSheet(item.cache || "", 150) || "-"; var ssl = sanitizeForSheet(item.ssl_expiry || "", 100); var sslDays = parseInt(item.ssl_days || "", 10); var bFile = sanitizeForSheet(item.backup_file || "", 200); // Chuyen YYYY-MM-DD_HH-MM sang DD-MM-YYYY_HH-MM neu con dinh dang cu bFile = bFile.replace(/^(\d{4})-(\d{2})-(\d{2})_(\d{2}-\d{2})/, "$3-$2-$1_$4"); // backup_link la chuoi nhieu link ngan cach nhau dau phay var bLinks = String(item.backup_link || "").trim().substring(0, 1000); keptGroups[vpsIp].items.push([ sanitizeForSheet(item.domain, 500), sanitizeForSheet(item.cms, 100), cache, webdb, ssl, { file: bFile, links: bLinks, days: isNaN(sslDays) ? null : sslDays, expired: ssl.indexOf("Het han") !== -1 }, timestamp ]); } // Neu la VPS moi chua co trong sheet thi them vao cuoi, nguoc lai giu nguyen vi tri cu if (groupOrder.indexOf(vpsIp) === -1) { groupOrder.push(vpsIp); } } // Tao outRows var outRows = []; var mergeList = []; var richColIdx = []; // vi tri dong can ghi RichText (cot backup = col 7, index 6) var sslColData = []; // [{days, expired}] cho mau SSL var currentStart = 2; for (var g = 0; g < groupOrder.length; g++) { var gIp = groupOrder[g]; var gData = keptGroups[gIp]; if (!gData || !gData.items || gData.items.length === 0) continue; var gCount = gData.items.length; for (var k = 0; k < gCount; k++) { var itm = gData.items[k]; // itm: [domain, cms, cache, webdb, ssl, backup_obj_or_str, timestamp] var sslVal = itm[4]; var bVal = itm[5]; var timeVal = itm[6]; var sslDaysVal = null; var sslExpired = false; if (bVal && typeof bVal === "object" && typeof bVal.days !== "undefined") { sslDaysVal = bVal.days; sslExpired = bVal.expired; } else { // du lieu cu tu VPS khac: kiem tra Het han va tinh lai so ngay neu la chuoi ngay thang var sslStr = String(sslVal || "").trim(); sslExpired = sslStr.indexOf("Het han") !== -1 || sslStr.indexOf("Khong co SSL") !== -1; var m = sslStr.match(/^([0-9]{2})-([0-9]{2})-([0-9]{4})$/); if (m) { var expDate = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1])); var nowDate = new Date(); sslDaysVal = Math.floor((expDate.getTime() - nowDate.getTime()) / (86400 * 1000)); } } outRows.push([ (k === 0) ? (gData.ipDisplay || gIp) : "", itm[0], // Col 2: domain itm[1], // Col 3: cms itm[2] || "-", // Col 4: Plugin/Cache itm[3], // Col 5: webdb sslVal, // Col 6: ssl "", // Col 7: backup - se ghi bang setRichTextValues timeVal // Col 8: timestamp ]); sslColData.push({ days: sslDaysVal, expired: sslExpired }); richColIdx.push({ bObj: bVal }); } if (gCount > 1) { mergeList.push({ startRow: currentStart, numRows: gCount }); } currentStart += gCount; } // Unmerge toan bo bang cu de tranh loi setValues neu tung co merge o cac cot if (lastRow > 1) { sheet.getRange(2, 1, lastRow - 1, Math.max(lastCol, HEADERS.length)).breakApart(); } // Xoa toan bo Data Validation (dropdown / quy tac xac thuc du lieu) de tranh loi setValues try { var maxR = Math.max(sheet.getMaxRows(), lastRow, (outRows ? outRows.length + 1 : 100)); var maxC = Math.max(sheet.getMaxColumns(), lastCol, HEADERS.length); sheet.getRange(1, 1, maxR, maxC).clearDataValidations(); } catch (eVal) { console.warn("[CANH BAO] Khong the xoa data validation: " + eVal.toString()); } // Neu sheet cu co nhieu hon 8 cot (vi du 9 cot cu co Bang thong), xoa trang cot du thua if (lastCol > HEADERS.length) { var extraCols = lastCol - HEADERS.length; sheet.getRange(1, HEADERS.length + 1, Math.max(lastRow, 1), extraCols).breakApart(); sheet.getRange(1, HEADERS.length + 1, Math.max(lastRow, 1), extraCols).clearContent(); sheet.getRange(1, HEADERS.length + 1, Math.max(lastRow, 1), extraCols).clearFormat(); } var plainRows = outRows; // da khong chua object if (plainRows.length > 0) { sheet.getRange(2, 1, plainRows.length, HEADERS.length).setValues(plainRows); // Ghi cot backup (col 8) bang RichText voi nhieu link var richValues = []; for (var bi = 0; bi < richColIdx.length; bi++) { var bObj = richColIdx[bi].bObj; var rtb = null; if (bObj && typeof bObj === "object" && bObj.file) { // Du lieu moi tu VPS hien tai: build RichTextValue voi links var linkArr = bObj.links ? bObj.links.split(",") : []; linkArr = linkArr.filter(function(l) { return l.trim() !== ""; }); // Ham tach remotename va url tu entry "remotename:https://..." function parseEntry(ent, fallbackName) { var s = ent.trim(); var ci = s.indexOf(":"); var prefix = ci > 0 ? s.substring(0, ci) : ""; var isProto = (prefix === "http" || prefix === "https"); if (ci > 0 && !isProto) { return { name: prefix, url: s.substring(ci + 1) }; } return { name: fallbackName, url: s }; } if (linkArr.length === 0) { rtb = SpreadsheetApp.newRichTextValue().setText(bObj.file).build(); } else if (linkArr.length === 1) { var p1 = parseEntry(linkArr[0], ""); var label1 = bObj.file + (p1.name ? "_" + p1.name : ""); var b1 = SpreadsheetApp.newRichTextValue().setText(label1); if (p1.url && /^https?:\/\//i.test(p1.url)) { try { b1.setLinkUrl(p1.url); } catch (eLink1) {} } rtb = b1.build(); } else { // Nhieu drive: moi dong "timestamp_remotename" link rieng var lines = []; for (var li = 0; li < linkArr.length; li++) { var p = parseEntry(linkArr[li], "Drive" + (li + 1)); lines.push({ label: bObj.file + "_" + p.name, url: p.url }); } var fullText = lines.map(function(x) { return x.label; }).join("\n"); var builder = SpreadsheetApp.newRichTextValue().setText(fullText); var pos = 0; for (var li2 = 0; li2 < lines.length; li2++) { var segLen = lines[li2].label.length; if (lines[li2].url && /^https?:\/\//i.test(lines[li2].url)) { try { builder.setLinkUrl(pos, pos + segLen, lines[li2].url); } catch (eLink2) {} } pos += segLen + 1; // +1 cho "\n" } rtb = builder.build(); } } else if (bObj && typeof bObj === "object" && typeof bObj.getText === "function") { // Du lieu cu tu VPS khac da la RichTextValue tren Sheet: GIU NGUYEN toan bo link va run var txt = bObj.getText(); if (!txt || txt.trim() === "") { rtb = SpreadsheetApp.newRichTextValue().setText("-").build(); } else { rtb = (typeof bObj.copy === "function") ? bObj.copy().build() : bObj; } } else if (bObj && typeof bObj === "object") { rtb = SpreadsheetApp.newRichTextValue().setText("-").build(); } else { // Du lieu cu dang chuoi thuan (fallback) rtb = SpreadsheetApp.newRichTextValue().setText(String(bObj || "-")).build(); } richValues.push([rtb]); } // Dinh dang bang truoc (vien, can le, mau nen cache, mau ssl, merge IP) formatTableBatch(sheet, plainRows, mergeList, sslColData); // Ghi cot Dung luong Web/DB (col 5) bang RichText: web mau xanh green, DB mau cam try { var styleWeb = SpreadsheetApp.newTextStyle().setForegroundColor(COLOR_WEB_SIZE).setBold(true).build(); var styleDb = SpreadsheetApp.newTextStyle().setForegroundColor(COLOR_DB_SIZE).setBold(true).build(); var styleSep = SpreadsheetApp.newTextStyle().setForegroundColor(COLOR_TEXT_MUTED).setBold(false).build(); var webDbRichValues = []; for (var wi = 0; wi < plainRows.length; wi++) { var rawWebDb = String(plainRows[wi][4] || "-"); webDbRichValues.push([buildWebDbRichText(rawWebDb, styleWeb, styleDb, styleSep)]); } sheet.getRange(2, 5, webDbRichValues.length, 1).setRichTextValues(webDbRichValues); } catch (eWebDb) { console.warn("[CANH BAO] Loi setRichTextValues cot Web/DB: " + eWebDb.toString()); } // Ghi cot backup (col 7) bang RichText voi nhieu link sheet.getRange(2, 7, richValues.length, 1).setRichTextValues(richValues); // Xoa note cu bang 1 lenh batch sheet.getRange(2, 7, richValues.length, 1).clearNote(); } else if (lastRow > 1) { var emptyRange = sheet.getRange(2, 1, lastRow - 1, HEADERS.length); emptyRange.breakApart(); emptyRange.clearContent(); emptyRange.clearFormat(); } // Xoa du lieu thua var oldDataCount = lastRow > 1 ? (lastRow - 1) : 0; if (oldDataCount > plainRows.length) { var tailStart = 2 + plainRows.length; var tailCount = oldDataCount - plainRows.length; var tailRange = sheet.getRange(tailStart, 1, tailCount, HEADERS.length); tailRange.breakApart(); tailRange.clearContent(); tailRange.clearFormat(); if (tailStart <= sheet.getMaxRows()) { sheet.deleteRows(tailStart, Math.min(tailCount, sheet.getMaxRows() - tailStart + 1)); } } return jsonResponse({ status: "success", vps_ip: vpsIp, provider: provider, count: sites.length, total_rows: plainRows.length, updated_at: timestamp }); } catch (error) { return jsonResponse({ status: "error", message: error.toString() }); } finally { lock.releaseLock(); } } /** * Khoi tao tieu de cot neu sheet rong hoac chua du cot */ function initHeaderIfNeeded(sheet) { var lastRow = sheet.getLastRow(); var maxCol = sheet.getMaxColumns(); // Dam bao sheet co it nhat 8 cot de khong bi loi out of bounds if (maxCol < HEADERS.length) { sheet.insertColumnsAfter(maxCol, HEADERS.length - maxCol); } // Neu sheet cu co nhieu hon 8 cot, xoa trang cac cot du thua o header if (maxCol > HEADERS.length && lastRow >= 1) { var extraHeaderCols = maxCol - HEADERS.length; sheet.getRange(1, HEADERS.length + 1, 1, extraHeaderCols).breakApart(); sheet.getRange(1, HEADERS.length + 1, 1, extraHeaderCols).clearContent(); sheet.getRange(1, HEADERS.length + 1, 1, extraHeaderCols).clearFormat(); } // Kiem tra xem header da co san va dung 8 cot chua de tranh goi lai ~18 lenh format if (lastRow >= 1 && maxCol >= HEADERS.length) { var curH = sheet.getRange(1, 1, 1, HEADERS.length).getValues()[0]; var isMatch = true; for (var h = 0; h < HEADERS.length; h++) { if (String(curH[h] || "").trim() !== HEADERS[h]) { isMatch = false; break; } } if (isMatch) { return; } } // Unmerge header cu neu co sheet.getRange(1, 1, 1, HEADERS.length).breakApart(); // Cap nhat header 8 cot sheet.getRange(1, 1, 1, HEADERS.length).setValues([HEADERS]); var headerRange = sheet.getRange(1, 1, 1, HEADERS.length); headerRange.setBackground(COLOR_HEADER_BG) .setFontColor(COLOR_HEADER_FG) .setFontWeight("bold") .setHorizontalAlignment("center") .setVerticalAlignment("middle") .setBorder(true, true, true, true, true, true, COLOR_BORDER, SpreadsheetApp.BorderStyle.SOLID); sheet.setRowHeight(1, HEADER_ROW_HEIGHT); sheet.setFrozenRows(1); for (var w = 0; w < COLUMN_WIDTHS.length; w++) { sheet.setColumnWidth(w + 1, COLUMN_WIDTHS[w]); } } /** * Dinh dang toan bo bang du lieu */ function formatTableBatch(sheet, outRows, mergeList, sslColData) { var numRows = outRows.length; if (numRows <= 0) return; // Dinh dang vien toan bo bang (header va cac dong du lieu) sheet.getRange(1, 1, numRows + 1, HEADERS.length) .setBorder(true, true, true, true, true, true, COLOR_BORDER, SpreadsheetApp.BorderStyle.SOLID); var totalRange = sheet.getRange(2, 1, numRows, HEADERS.length); totalRange.setFontFamily("Arial") .setFontSize(10) .setVerticalAlignment("middle"); // Can le: // IP (1): Center, wrap // Domain (2): Left // Ma nguon (3): Center // Plugin/Cache (4): Center // Dung luong (5): Center // Han SSL (6): Center // Backup (7): Left // Cap nhat luc (8): Center sheet.getRange(2, 1, numRows, 1).setHorizontalAlignment("center").setWrap(true); sheet.getRange(2, 2, numRows, 1).setHorizontalAlignment("left").setWrap(true); sheet.getRange(2, 3, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 4, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 5, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 6, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 7, numRows, 1).setHorizontalAlignment("left"); sheet.getRange(2, 8, numRows, 1).setHorizontalAlignment("center"); // To mau nen so le cho cac dong du lieu var tableBgs = []; for (var r = 0; r < numRows; r++) { var rowBg = (r % 2 === 0) ? COLOR_ROW_EVEN : COLOR_ROW_ODD; var rowColors = []; for (var col = 0; col < HEADERS.length; col++) { rowColors.push(rowBg); } tableBgs.push(rowColors); } totalRange.setBackgrounds(tableBgs); // Dinh dang chu & kieu chu cot Plugin/Cache (4) - dung chung mau nen so le var cacheFg = [], cacheFw = []; for (var c = 0; c < numRows; c++) { var cacheStr = String(outRows[c][3] || ""); if (cacheStr.indexOf("HIT") !== -1) { cacheFg.push([COLOR_CACHE_HIT_FG]); cacheFw.push(["bold"]); } else if (cacheStr.indexOf("ERR") !== -1) { cacheFg.push([COLOR_CACHE_ERR_FG]); cacheFw.push(["bold"]); } else if (cacheStr.indexOf("MISS") !== -1) { cacheFg.push([COLOR_CACHE_MISS_FG]); cacheFw.push(["bold"]); } else if (cacheStr === "-") { cacheFg.push([COLOR_TEXT_MUTED]); cacheFw.push(["normal"]); } else { cacheFg.push([COLOR_TEXT_NORMAL]); cacheFw.push(["normal"]); } } sheet.getRange(2, 4, numRows, 1) .setFontColors(cacheFg) .setFontWeights(cacheFw); // To mau cot SSL (6) theo ngay con lai var bg = [], fg = [], fw = []; for (var r = 0; r < numRows; r++) { var rowBg = (r % 2 === 0) ? COLOR_ROW_EVEN : COLOR_ROW_ODD; var ssl = sslColData[r] || {}; var days = ssl.days; var expired = ssl.expired; var sslStr = String(outRows[r][5] || ""); // col 6 la index 5 trong outRows if (expired || sslStr.indexOf("Het han") !== -1 || sslStr.indexOf("Khong co SSL") !== -1) { // Do - het han bg.push([COLOR_SSL_EXP_BG]); fg.push([COLOR_SSL_EXP_FG]); fw.push(["bold"]); } else if (days !== null && days !== undefined && days < 10) { // Vang - sap het han (< 10 ngay) bg.push([COLOR_SSL_WARN_BG]); fg.push([COLOR_SSL_WARN_FG]); fw.push(["bold"]); } else { bg.push([rowBg]); fg.push([COLOR_TEXT_NORMAL]); fw.push(["normal"]); } } sheet.getRange(2, 6, numRows, 1) .setBackgrounds(bg) .setFontColors(fg) .setFontWeights(fw); // Gop o cot IP (col 1) cho tung VPS nhieu web if (mergeList && mergeList.length > 0) { for (var m = 0; m < mergeList.length; m++) { var item = mergeList[m]; sheet.getRange(item.startRow, 1, item.numRows, 1).merge(); } } } /** * Chay thu cong tu Apps Script (hoac menu LBK Script tren Google Sheet) * de dinh dang lai toan bo mau nen so le, border va can le cho Sheet hien tai */ function dinhDangSheet() { var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheetByName(SHEET_NAME); if (!sheet) { Logger.log("Khong tim thay tab: " + SHEET_NAME); return; } // Dinh dang Header initHeaderIfNeeded(sheet); sheet.setRowHeight(1, HEADER_ROW_HEIGHT); for (var w = 0; w < COLUMN_WIDTHS.length; w++) { sheet.setColumnWidth(w + 1, COLUMN_WIDTHS[w]); } var lastRow = sheet.getLastRow(); if (lastRow < 2) { Logger.log("Chua co du lieu de dinh dang."); return; } var numRows = lastRow - 1; var totalRange = sheet.getRange(2, 1, numRows, HEADERS.length); try { totalRange.clearDataValidations(); } catch (eVal) {} var values = totalRange.getValues(); // Dinh dang vien toan bo bang (header + du lieu) sheet.getRange(1, 1, lastRow, HEADERS.length) .setBorder(true, true, true, true, true, true, COLOR_BORDER, SpreadsheetApp.BorderStyle.SOLID); totalRange.setFontFamily("Arial") .setFontSize(10) .setVerticalAlignment("middle"); // Can le sheet.getRange(2, 1, numRows, 1).setHorizontalAlignment("center").setWrap(true); sheet.getRange(2, 2, numRows, 1).setHorizontalAlignment("left").setWrap(true); sheet.getRange(2, 3, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 4, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 5, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 6, numRows, 1).setHorizontalAlignment("center"); sheet.getRange(2, 7, numRows, 1).setHorizontalAlignment("left"); sheet.getRange(2, 8, numRows, 1).setHorizontalAlignment("center"); // To mau so le cho toan bo cac dong var tableBgs = []; for (var r = 0; r < numRows; r++) { var rowBg = (r % 2 === 0) ? COLOR_ROW_EVEN : COLOR_ROW_ODD; var rowColors = []; for (var col = 0; col < HEADERS.length; col++) { rowColors.push(rowBg); } tableBgs.push(rowColors); } totalRange.setBackgrounds(tableBgs); // Dinh dang chu & kieu chu cot Plugin/Cache (col 4) - dung chung mau nen so le var cacheFg = [], cacheFw = []; for (var c = 0; c < numRows; c++) { var cacheStr = String(values[c][3] || ""); if (cacheStr.indexOf("HIT") !== -1) { cacheFg.push([COLOR_CACHE_HIT_FG]); cacheFw.push(["bold"]); } else if (cacheStr.indexOf("ERR") !== -1) { cacheFg.push([COLOR_CACHE_ERR_FG]); cacheFw.push(["bold"]); } else if (cacheStr.indexOf("MISS") !== -1) { cacheFg.push([COLOR_CACHE_MISS_FG]); cacheFw.push(["bold"]); } else if (cacheStr === "-") { cacheFg.push([COLOR_TEXT_MUTED]); cacheFw.push(["normal"]); } else { cacheFg.push([COLOR_TEXT_NORMAL]); cacheFw.push(["normal"]); } } sheet.getRange(2, 4, numRows, 1) .setFontColors(cacheFg) .setFontWeights(cacheFw); // To mau cot SSL (col 6) var sslBg = [], sslFg = [], sslFw = []; var nowDate = new Date(); for (var s = 0; s < numRows; s++) { var sRowBg = (s % 2 === 0) ? COLOR_ROW_EVEN : COLOR_ROW_ODD; var sslStr = String(values[s][5] || "").trim(); var isExpired = sslStr.indexOf("Het han") !== -1 || sslStr.indexOf("Khong co SSL") !== -1; var daysLeft = null; var matchDate = sslStr.match(/^([0-9]{2})-([0-9]{2})-([0-9]{4})$/); if (matchDate) { var expD = new Date(Number(matchDate[3]), Number(matchDate[2]) - 1, Number(matchDate[1])); daysLeft = Math.floor((expD.getTime() - nowDate.getTime()) / (86400 * 1000)); } if (isExpired) { sslBg.push([COLOR_SSL_EXP_BG]); sslFg.push([COLOR_SSL_EXP_FG]); sslFw.push(["bold"]); } else if (daysLeft !== null && daysLeft < 10) { sslBg.push([COLOR_SSL_WARN_BG]); sslFg.push([COLOR_SSL_WARN_FG]); sslFw.push(["bold"]); } else { sslBg.push([sRowBg]); sslFg.push([COLOR_TEXT_NORMAL]); sslFw.push(["normal"]); } } sheet.getRange(2, 6, numRows, 1) .setBackgrounds(sslBg) .setFontColors(sslFg) .setFontWeights(sslFw); Logger.log("Da dinh dang lai " + numRows + " dong thanh cong!"); } /** * Tu dong them menu tren Google Sheet khi mo file */ function onOpen() { try { SpreadsheetApp.getUi() .createMenu("LBK Script") .addItem("Dinh dang lai bang tinh", "dinhDangSheet") .addToUi(); } catch (e) {} } /** * GET de test ket noi */ function doGet(e) { var secNote = SECRET_KEY !== "" ? "Da bat bao ve bang SECRET_KEY" : "<span style='color:red;'>Chua bat SECRET_KEY</span>"; var html = "<h3>LBK Script - Google Sheet Sync Webhook</h3>" + "<p>Trang thai: <b>Hoat dong & San sang</b></p>" + "<p>Bao mat: " + secNote + "</p>" + "<p>Vui long gui POST (JSON) tu VPS de dong bo danh sach website.</p>"; return HtmlService.createHtmlOutput(html); }

Tạo Google OAuth Client ID riêng cho Rclone (khắc phục lỗi 403 Quota exceeded)

Rclone mặc định dùng chung Client ID cộng đồng (project 202264815644) với hàng nghìn người khác, nên vào giờ cao điểm backup (0h – 2h sáng) Google Drive rất dễ trả về Error 403: Quota exceeded for quota metric 'Queries' / rateLimitExceeded (giới hạn quota Drive API). Việc tạo Client ID riêng là miễn phí, mất khoảng 5–7 phút, chỉ làm 1 lần duy nhất rồi dùng chung cho mọi VPS và mọi tài khoản Drive (gdrive, gdrive_2...). Sau khi làm xong, backup sẽ hết lỗi 403, hết retry vòng lặp và tốc độ upload ổn định hơn hẳn. Tài liệu gốc của rclone về việc này xem tại rclone.org/drive — Making your own Client ID.

Dấu hiệu nhận biết bạn đang dính lỗi này

Mở log /var/log/backup.log thấy dòng Failed to copy: googleapi: Error 403 ... rateLimitExceeded kèm Attempt 1/5 failed. Con số tổng dung lượng trong log cứ phình to dần (ví dụ file nén thật chỉ 453 MB nhưng log báo 907 MB → 1.3 GB → 3 GB → 6 GB). Đây không phải file backup bị phình — đó chỉ là số liệu cộng dồn của rclone qua mỗi lần retry thất bại rồi upload lại từ đầu. File .tar.gz trên VPS và trên Drive (nếu upload thành công) vẫn chỉ ~453 MB.

1

Bước 1 — Tạo / chọn Project trên Google Cloud Console

1. Trên máy tính, mở trình duyệt vào console.cloud.google.com và đăng nhập bằng Gmail chính chủ (Gmail này nên trùng với Gmail sẽ chứa file backup). Có thể vào thẳng trang tạo Project mới để bỏ qua vài click.
2. Bấm vào menu chọn Project ở góc trái trên cùng (kế bên logo Google Cloud) -> bấm New Project.
3. Đặt tên gợi nhớ, ví dụ lbk-backup, phần Organization cứ để No organization -> bấm Create.
4. Chờ khoảng 10–20 giây cho Google tạo xong, sau đó bấm lại menu Project và chọn đúng project vừa tạo (tên project phải hiện trên thanh tiêu đề thì các bước sau mới áp vào đúng chỗ).

2

Bước 2 — Bật Google Drive API cho Project

1. Trong cùng project vừa tạo, vào menu APIs & Services -> Library (hoặc bấm vào đường dẫn trực tiếp này).
2. Gõ tìm Google Drive API -> bấm vào kết quả -> bấm nút Enable (Bật).
3. Chờ vài giây đến khi nút chuyển thành Manage (Quản lý) nghĩa là API đã bật. Bước này không cần tạo API Key — rclone dùng OAuth nên chỉ cần Client ID ở bước 4.

3

Bước 3 — Cấu hình OAuth Consent Screen (chỉ làm 1 lần duy nhất)

1. Bấm vào trang OAuth consent screen (hoặc vào menu APIs & Services -> OAuth consent screen) -> mục User Type chọn External (Bên ngoài) -> bấm Create. Chi tiết các trường xem thêm tài liệu Consent Screen của Google.
2. Màn hình App information: App name điền LBK Backup; User support email chọn Gmail của bạn; Developer contact information nhập lại đúng Gmail đó -> bấm Save and Continue (Lưu và tiếp tục).
3. Màn hình Scopes: bấm Add or Remove Scopes (Thêm hoặc xóa phạm vi) -> tick chọn phạm vi https://www.googleapis.com/auth/drive (toàn quyền Google Drive để rclone có thể upload, lấy link và xóa bản cũ xoay vòng KEEP; danh sách scope đầy đủ xem tại bảng OAuth 2.0 Scopes for Google APIs) -> bấm Update -> Save and Continue.
4. Màn hình Test users: bấm Add Users -> nhập Gmail sẽ cấp quyền và chứa file backup (thường chính là Gmail của bạn, có thể thêm nhiều Gmail nếu dùng nhiều remote) -> Add -> Save and Continue -> Back to Dashboard.
5. Trạng thái mặc định là Testing (đang kiểm thử) — dùng tốt cho backup cá nhân. Nếu mỗi lần đăng nhập Google hiện cảnh báo “Google hasn't verified this app” gây khó chịu, hãy bấm PUBLISH APP (Xuất bản ứng dụng) để chuyển sang Production; không bắt buộc và không ảnh hưởng quota.

4

Bước 4 — Tạo OAuth Client ID + Client Secret

1. Vào thẳng trang tạo OAuth client ID (hoặc menu APIs & Services -> Credentials (Thông tin xác thực) -> bấm Create Credentials -> chọn OAuth client ID; hướng dẫn chính thức tại tạo OAuth client ID của Google Cloud).
2. Mục Application type (Loại ứng dụng) bắt buộc chọn Desktop app (Ứng dụng máy tính) — chỉ loại này mới tương thích với lệnh rclone authorize; nếu chọn Web application sẽ báo lỗi redirect_uri_mismatch. Đặt Name là rclone-lbk cho dễ nhớ -> bấm Create.
3. Hộp thoại hiện ra 2 chuỗi, hãy copy và lưu cẩn thận vào trình quản lý mật khẩu:

Client ID: 123456789xxx-abc...xyz.apps.googleusercontent.com
Client Secret: GOCSPX-xxxxxxxxxxxxxxxx

Nếu lỡ đóng mất, vào lại trang Credentials -> bấm vào tên client rclone-lbk để xem lại cả hai. Tuyệt đối không đăng 2 chuỗi này lên Git, Facebook hay gửi cho người lạ — ai có chúng đều có thể mạo danh app của bạn gọi API tốn quota.

5

Bước 5 — Nhập Client ID vào LBK trên VPS khi thêm tài khoản Drive

1. SSH vào VPS với quyền root, gõ lệnh lbk -> mở Google Drive Manager -> [1] Thêm / Cài đặt tài khoản Google Drive (script rclone_gdrive.sh; cách rclone kết nối Drive nói chung xem tại tài liệu rclone Google Drive).
2. Đặt tên remote gợi nhớ theo Gmail, ví dụ gdrive cho Gmail chính, gdrive_2 cho Gmail phụ.
3. Khi script hỏi Nhập Client ID: dán đúng Client ID đã copy ở Bước 4; khi hỏi Nhập Client Secret: dán Secret tương ứng. Nếu bỏ trống cả hai, script sẽ dùng app chung của rclone và lỗi 403 sẽ quay lại.
4. Một cặp Client ID/Secret này dùng chung được cho mọi remote và mọi VPS — mỗi Gmail khác nhau chỉ cần tạo remote mới và lấy token riêng (Bước 6), không cần tạo Client ID mới.

6

Bước 6 — Lấy Token xác thực trên máy tính cá nhân (vì VPS không có trình duyệt)

1. Trên máy tính Windows/macOS của bạn, tải và cài rclone tại rclone.org/downloads (chọn đúng bản cho hệ điều hành; cách dùng lệnh authorize xem tại tài liệu rclone authorize).
2. Mở CMD (Windows) hoặc Terminal (macOS/Linux) và chạy đúng lệnh sau — nhớ thay 2 giá trị trong ngoặc bằng ID/Secret thật của bạn:

rclone authorize "drive" "DAN_CLIENT_ID_CUA_BAN" "DAN_CLIENT_SECRET_CUA_BAN"

3. Trình duyệt sẽ tự bật lên -> đăng nhập đúng Gmail sẽ chứa file backup -> bấm Tiếp tục / Cho phép để cấp quyền Drive cho app LBK Backup. Nếu Google hiện cảnh báo “Google hasn't verified this app”, bấm Advanced (Nâng cao) -> Go to LBK Backup (unsafe) rồi cho phép tiếp — đây là app tự bạn tạo nên an toàn.
4. Quay lại cửa sổ CMD/Terminal, copy toàn bộ đoạn JSON bắt đầu bằng {"access_token":"ya29... (rất dài, phải copy đủ từ dấu { đầu đến dấu } cuối, không xuống dòng giữa chừng) -> quay lại VPS, dán vào prompt Dán JSON token vào đây rồi Enter.
5. Nhập tiếp Folder ID nếu chỉ muốn backup vào 1 thư mục Drive cụ thể, hoặc để trống để dùng toàn bộ Drive. Script sẽ tự kiểm tra kết nối và báo [OK] Kết nối Google Drive thành công!.

7

Bước 7 — Kiểm tra kết nối & xử lý lỗi thường gặp

Chạy 2 lệnh sau trên VPS (thay gdrive bằng tên remote của bạn). Nếu cả hai đều liệt kê được file và dung lượng là thành công:
rclone lsf gdrive: --max-depth 1 và rclone about gdrive:.

Lỗi redirect_uri_mismatch khi authorize: bạn đã chọn nhầm loại Web application ở Bước 4 — hãy tạo lại một OAuth Client ID mới loại Desktop app rồi authorize lại.
Lỗi 403 access_denied / Error 400: Gmail đó chưa được thêm vào Test users ở Bước 3 — quay lại Consent Screen bổ sung rồi authorize lại từ đầu.
Lỗi token không chứa access_token: bạn copy thiếu đoạn JSON — phải copy từ dấu { đầu tiên đến dấu } cuối cùng.
Vẫn báo 403 rateLimitExceeded dù đã có ID riêng: nguyên nhân còn lại thường là cron backup nhiều site dồn cùng 1 khung giờ — hãy giãn lịch cron các site lệch nhau 15–30 phút, giữ nguyên cấu hình chống nghẽn mới của LBK (--tpslimit 2 --drive-pacer-min-sleep 200ms) và thử lại sau 5–10 phút. Chi tiết cơ chế quota và backoff xem tại tài liệu Usage limits của Google Drive.

Mẹo vận hành lâu dài

Chỉ cần 1 cặp Client ID/Secret duy nhất cho toàn bộ hệ thống — dùng chung cho mọi VPS và mọi tài khoản Drive để dễ quản lý quota. Hãy lưu chúng vào trình quản lý mật khẩu (Bitwarden, 1Password...) thay vì ghi chú rời rạc. Nếu Drive sắp đầy, đừng tăng KEEP quá cao vì rclone sẽ phải gọi API nhiều hơn và dễ retry. Với LBK, sau khi đổi sang Client ID riêng, bạn có thể cho chạy lại backup ban đêm và theo dõi log: chỉ cần thấy [OK] Upload thành công là xong, không cần quan tâm con số GB trung gian trong lúc retry.

Kiểm tra sự hiện diện của tất cả 32 tập tin trên máy chủ phân phối tại thư mục install/ và install/scripts/:
Tên Tập Tin Trạng Thái Dung Lượng Cập Nhật Mô Tả Nhiệm Vụ
install.sh [OK] Sẵn sàng 5,546 B 04/10/2026 14:29 Script cài đặt gốc curl tải về để khởi tạo hệ thống
backup_all.sh [OK] Sẵn sàng 36,329 B 04/10/2026 14:29 Sao lưu toàn bộ website & database lên Google Drive
backup_crontab.sh [OK] Sẵn sàng 3,580 B 04/10/2026 14:29 Đặt lịch backup tự động theo giờ Việt Nam (GMT+7)
backup_one.sh [OK] Sẵn sàng 34,057 B 04/10/2026 14:29 Sao lưu 1 website & database chỉ định
fail2ban.sh [OK] Sẵn sàng 42,214 B 04/10/2026 14:29 Logic quản lý bảo mật & chống brute-force Fail2ban
google_apps_script.js [OK] Sẵn sàng 34,808 B 04/10/2026 14:29 Mã Webhook Apps Script đồng bộ danh sách web lên Google Sheet
lib_backup_db.sh [OK] Sẵn sàng 15,287 B 04/10/2026 14:30 Thư viện hỗ trợ Multi-Database Engine (MySQL, MongoDB, PostgreSQL)
lib_site_scan.sh [OK] Sẵn sàng 2,708 B 04/10/2026 14:30 Thư viện quét và phân loại thư mục website & tài sản dùng chung
menu.sh [OK] Sẵn sàng 4,873 B 04/10/2026 14:30 Menu quản trị chính (lbk / LBK)
menu_backup.sh [OK] Sẵn sàng 28,907 B 04/10/2026 14:30 Menu giao diện quản lý sao lưu
menu_fail2ban.sh [OK] Sẵn sàng 8,027 B 04/10/2026 14:30 Menu giao diện quản lý Fail2ban
menu_find_large_files.sh [OK] Sẵn sàng 14,115 B 04/10/2026 14:30 Menu giao diện tìm kiếm & quản lý file dung lượng lớn
menu_optimize_images.sh [OK] Sẵn sàng 9,526 B 04/10/2026 14:30 Menu giao diện quản lý tối ưu ảnh website
menu_phanquyen.sh [OK] Sẵn sàng 5,971 B 04/10/2026 14:30 Menu giao diện phân quyền & Pure-FTPd
menu_rclone_gdrive.sh [OK] Sẵn sàng 7,112 B 04/10/2026 14:30 Menu giao diện kết nối Google Drive Rclone
menu_restore.sh [OK] Sẵn sàng 2,002 B 04/10/2026 14:30 Menu giao diện khôi phục website
menu_security_web.sh [OK] Sẵn sàng 6,215 B 04/10/2026 14:30 Menu giao diện quản lý bảo mật Web Server
menu_swap.sh [OK] Sẵn sàng 24,025 B 04/10/2026 14:31 Menu giao diện quản lý & tối ưu bộ nhớ ảo Swap
menu_sync_sheet.sh [OK] Sẵn sàng 15,932 B 04/10/2026 14:31 Menu giao diện quản lý đồng bộ Google Sheet
menu_system_info.sh [OK] Sẵn sàng 26,753 B 04/10/2026 14:31 Menu giao diện xem toàn bộ thông tin cấu hình hệ thống & LBK
optimize_images.sh [OK] Sẵn sàng 10,146 B 04/10/2026 14:31 Worker tối ưu & giảm dung lượng ảnh JPG/PNG trực tiếp
phanquyen_all.sh [OK] Sẵn sàng 8,417 B 04/10/2026 14:31 Phân quyền & đồng bộ FTP tất cả website
phanquyen_one.sh [OK] Sẵn sàng 9,049 B 04/10/2026 14:31 Phân quyền & đồng bộ FTP 1 website
rclone_gdrive.sh [OK] Sẵn sàng 5,985 B 04/10/2026 14:31 Cài đặt & cấu hình Rclone Google Drive
rclone_gdrive_root_folder.sh [OK] Sẵn sàng 3,513 B 04/10/2026 14:31 Đổi Root Folder ID Google Drive
restore_site.sh [OK] Sẵn sàng 50,985 B 04/10/2026 14:31 Khôi phục website & database an toàn từ Google Drive
security_web.sh [OK] Sẵn sàng 33,563 B 04/10/2026 14:31 Logic bảo mật Web Server, chống DDoS Throttling & XML-RPC
sync_sheet.sh [OK] Sẵn sàng 44,966 B 04/10/2026 14:32 Thu thập thông tin web (CMS, Cache, SSL, Backup) & POST lên Google Sheet
uninstall.sh [OK] Sẵn sàng 19,237 B 04/10/2026 14:32 Gỡ cài đặt LBK Script sạch sẽ khỏi hệ thống
update.sh [OK] Sẵn sàng 4,200 B 04/10/2026 14:32 Cập nhật LBK Script lên phiên bản mới nhất
watch_phanquyen.sh [OK] Sẵn sàng 21,303 B 04/10/2026 14:32 Tự động phân quyền website thời gian thực (inotifywait daemon)
watch_wwwroot.sh [OK] Sẵn sàng 10,661 B 04/10/2026 14:32 Theo dõi thay đổi webroot để đồng bộ tức thì

Health Check Endpoint (JSON)

Dùng để giám sát từ xa hoặc tích hợp vào hệ thống CI/CD / Uptime Kuma để kiểm tra toàn bộ 29 file phân phối:

https://script.lbk.vn/?api=health Mở JSON
Phản hồi JSON mẫu:
{
  "status": "healthy",
  "install_sh": true,
  "total_files": 29,
  "ready_files": 29,
  "server_time": "2026-10-18 12:27:04"
}