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.
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.
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:
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:
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