|
|
@@ -24,6 +24,8 @@
|
|
|
#include <sstream>
|
|
|
#include <sys/stat.h>
|
|
|
#include <thread>
|
|
|
+#include <unistd.h>
|
|
|
+#include <vector>
|
|
|
|
|
|
namespace ngs {
|
|
|
namespace weblog {
|
|
|
@@ -736,6 +738,85 @@ double to_d(const std::string& s) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+bool is_db_filename(const std::string& name) {
|
|
|
+ return name == "weblog.db" || name.rfind("weblog.db-", 0) == 0;
|
|
|
+}
|
|
|
+
|
|
|
+int64_t file_bytes(const std::string& path) {
|
|
|
+ struct stat st {};
|
|
|
+ if (stat(path.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return static_cast<int64_t>(st.st_size);
|
|
|
+}
|
|
|
+
|
|
|
+bool stamp_file_meta(ylib::sqlite3& db, const std::string& path,
|
|
|
+ const std::string& offset_key, const std::string& inode_key,
|
|
|
+ const std::string& size_key) {
|
|
|
+ struct stat st {};
|
|
|
+ if (stat(path.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
|
+ return meta_set(db, offset_key, "0") && meta_set(db, inode_key, "") &&
|
|
|
+ meta_set(db, size_key, "0");
|
|
|
+ }
|
|
|
+ const std::string inode_s =
|
|
|
+ std::to_string(static_cast<unsigned long long>(st.st_ino));
|
|
|
+ const std::string size_s =
|
|
|
+ std::to_string(static_cast<unsigned long long>(st.st_size));
|
|
|
+ return meta_set(db, offset_key, size_s) && meta_set(db, inode_key, inode_s) &&
|
|
|
+ meta_set(db, size_key, size_s);
|
|
|
+}
|
|
|
+
|
|
|
+bool recreate_empty_file(const std::string& path) {
|
|
|
+ std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
|
+ return static_cast<bool>(out);
|
|
|
+}
|
|
|
+
|
|
|
+void unlink_log_dir_files(const std::string& dir, bool drop_files, bool drop_db) {
|
|
|
+ DIR* d = opendir(dir.c_str());
|
|
|
+ if (!d) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ std::vector<std::string> names;
|
|
|
+ while (dirent* ent = readdir(d)) {
|
|
|
+ if (!ent || !ent->d_name) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::string fname = ent->d_name;
|
|
|
+ if (fname == "." || fname == "..") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const bool db_file = is_db_filename(fname);
|
|
|
+ if ((drop_db && db_file) || (drop_files && !db_file)) {
|
|
|
+ names.push_back(fname);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ closedir(d);
|
|
|
+ for (const auto& fname : names) {
|
|
|
+ const std::string path = join_path(dir, fname);
|
|
|
+ if (!is_dir(path)) {
|
|
|
+ remove_path(path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool recreate_empty_db(const std::string& name, const std::string& dir,
|
|
|
+ std::string& err) {
|
|
|
+ ylib::sqlite3 db;
|
|
|
+ if (!open_site_db(name, db, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!ensure_schema(db)) {
|
|
|
+ err = std::string("重建日志库失败: ") + db.last_error();
|
|
|
+ db.close();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ stamp_file_meta(db, join_path(dir, "access.log"), "offset", "inode", "size");
|
|
|
+ stamp_file_meta(db, join_path(dir, "error.log"), "error_offset",
|
|
|
+ "error_inode", "error_size");
|
|
|
+ db.close();
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
} // namespace
|
|
|
|
|
|
std::string legacy_db_dir() {
|
|
|
@@ -823,6 +904,82 @@ bool remove_db(const std::string& name) {
|
|
|
return remove_path(path);
|
|
|
}
|
|
|
|
|
|
+LogUsage usage(const std::string& name) {
|
|
|
+ LogUsage u;
|
|
|
+ if (!is_safe_site_name(name)) {
|
|
|
+ return u;
|
|
|
+ }
|
|
|
+ const std::string dir = join_path(sites_log_dir(), name);
|
|
|
+ DIR* d = opendir(dir.c_str());
|
|
|
+ if (!d) {
|
|
|
+ return u;
|
|
|
+ }
|
|
|
+ while (dirent* ent = readdir(d)) {
|
|
|
+ if (!ent || !ent->d_name) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::string fname = ent->d_name;
|
|
|
+ if (fname == "." || fname == "..") {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const std::string path = join_path(dir, fname);
|
|
|
+ const int64_t sz = file_bytes(path);
|
|
|
+ if (is_db_filename(fname)) {
|
|
|
+ u.db_bytes += sz;
|
|
|
+ } else {
|
|
|
+ u.file_bytes += sz;
|
|
|
+ if (fname == "access.log") {
|
|
|
+ u.access_bytes = sz;
|
|
|
+ } else if (fname == "error.log") {
|
|
|
+ u.error_bytes = sz;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ closedir(d);
|
|
|
+ return u;
|
|
|
+}
|
|
|
+
|
|
|
+bool purge(const std::string& name, const PurgeOptions& opt, std::string& err) {
|
|
|
+ if (!opt.files && !opt.db) {
|
|
|
+ err = "请选择要清理的内容";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!is_safe_site_name(name)) {
|
|
|
+ err = "无效站点名";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ std::lock_guard<std::mutex> lock(mu());
|
|
|
+ const std::string dir = join_path(sites_log_dir(), name);
|
|
|
+ if (!ensure_dir(dir)) {
|
|
|
+ err = "无法创建日志目录: " + dir;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ unlink_log_dir_files(dir, opt.files, opt.db);
|
|
|
+ if (opt.files) {
|
|
|
+ if (!recreate_empty_file(join_path(dir, "access.log")) ||
|
|
|
+ !recreate_empty_file(join_path(dir, "error.log"))) {
|
|
|
+ err = "重建日志文件失败";
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (opt.db) {
|
|
|
+ if (!recreate_empty_db(name, dir, err)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ } else if (opt.files) {
|
|
|
+ ylib::sqlite3 db;
|
|
|
+ std::string open_err;
|
|
|
+ if (open_site_db(name, db, open_err) && ensure_schema(db)) {
|
|
|
+ stamp_file_meta(db, join_path(dir, "access.log"), "offset", "inode",
|
|
|
+ "size");
|
|
|
+ stamp_file_meta(db, join_path(dir, "error.log"), "error_offset",
|
|
|
+ "error_inode", "error_size");
|
|
|
+ }
|
|
|
+ db.close();
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
void ingest_once() {
|
|
|
std::vector<website::SiteInfo> sites;
|
|
|
try {
|