125 lines
3.3 KiB
D
125 lines
3.3 KiB
D
|
module data;
|
||
|
|
||
|
import std.file;
|
||
|
import std.datetime;
|
||
|
import std.format;
|
||
|
import d2sqlite3;
|
||
|
|
||
|
static immutable FS_ORIGIN = "_LOCAL_FILESYSTEM_";
|
||
|
|
||
|
struct StoredSession {
|
||
|
long id;
|
||
|
SysTime startTimestamp;
|
||
|
SysTime endTimestamp;
|
||
|
string url;
|
||
|
string fullUrl;
|
||
|
string userAgent;
|
||
|
long eventCount;
|
||
|
}
|
||
|
|
||
|
void storeSession(StoredSession s) {
|
||
|
string origin = extractOrigin(s.url);
|
||
|
if (origin is null) {
|
||
|
throw new Exception("Unable to parse origin from url: " ~ s.url);
|
||
|
} else if (origin.length == 0) {
|
||
|
origin = FS_ORIGIN;
|
||
|
}
|
||
|
Database db = getOrCreateDatabase(origin);
|
||
|
Statement stmt = db.prepare(
|
||
|
"INSERT INTO session " ~
|
||
|
"(start_timestamp, end_timestamp, url, full_url, user_agent, event_count) " ~
|
||
|
"VALUES (?, ?, ?, ?, ?, ?)"
|
||
|
);
|
||
|
stmt.bind(1, formatTimestamp(s.startTimestamp));
|
||
|
stmt.bind(2, formatTimestamp(s.endTimestamp));
|
||
|
stmt.bind(3, s.url);
|
||
|
stmt.bind(4, s.fullUrl);
|
||
|
stmt.bind(5, s.userAgent);
|
||
|
stmt.bind(6, s.eventCount);
|
||
|
stmt.execute();
|
||
|
}
|
||
|
|
||
|
Database getOrCreateDatabase(string origin) {
|
||
|
string filename = dbPath(origin);
|
||
|
if (!exists(filename)) {
|
||
|
initDb(filename);
|
||
|
}
|
||
|
return Database(filename, SQLITE_OPEN_READWRITE);
|
||
|
}
|
||
|
|
||
|
private void initDb(string path) {
|
||
|
if (exists(path)) std.file.remove(path);
|
||
|
Database db = Database(path);
|
||
|
db.run(q"SQL
|
||
|
CREATE TABLE session (
|
||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
start_timestamp TEXT NOT NULL,
|
||
|
end_timestamp TEXT NOT NULL,
|
||
|
url TEXT NOT NULL,
|
||
|
full_url TEXT NOT NULL,
|
||
|
user_agent TEXT NOT NULL,
|
||
|
event_count INTEGER NOT NULL
|
||
|
);
|
||
|
SQL"
|
||
|
);
|
||
|
}
|
||
|
|
||
|
ulong countSessions(string origin) {
|
||
|
Database db = getOrCreateDatabase(origin);
|
||
|
return db.execute("SELECT COUNT(id) FROM session;").oneValue!ulong;
|
||
|
}
|
||
|
|
||
|
private string formatTimestamp(SysTime t) {
|
||
|
return format!"%04d-%02d-%02d %02d:%02d:%02d"(
|
||
|
t.year, t.month, t.day,
|
||
|
t.hour, t.minute, t.second
|
||
|
);
|
||
|
}
|
||
|
|
||
|
string extractOrigin(string url) {
|
||
|
import std.algorithm : countUntil, startsWith;
|
||
|
ptrdiff_t idx = countUntil(url, "://");
|
||
|
if (idx == -1) return null;
|
||
|
string origin = url[idx + 3 .. $];
|
||
|
ptrdiff_t trailingSlashIdx = countUntil(origin, "/");
|
||
|
if (trailingSlashIdx != -1) {
|
||
|
origin = origin[0 .. trailingSlashIdx];
|
||
|
}
|
||
|
if (startsWith(origin, "www.")) {
|
||
|
origin = origin[4 .. $];
|
||
|
}
|
||
|
return origin;
|
||
|
}
|
||
|
|
||
|
unittest {
|
||
|
assert(extractOrigin("https://www.google.com/search") == "google.com");
|
||
|
assert(extractOrigin("https://litelist.andrewlalis.com") == "litelist.andrewlalis.com");
|
||
|
}
|
||
|
|
||
|
string dbPath(string origin) {
|
||
|
return "sitestat-db_" ~ origin ~ ".sqlite";
|
||
|
}
|
||
|
|
||
|
string originFromDbPath(string path) {
|
||
|
import std.algorithm : countUntil;
|
||
|
ptrdiff_t idx = countUntil(path, "sitestat-db_");
|
||
|
if (idx == -1) return null;
|
||
|
return path[(idx + 12)..$-7];
|
||
|
}
|
||
|
|
||
|
unittest {
|
||
|
assert(originFromDbPath("sitestat-db__LOCAL_FILESYSTEM_.sqlite") == "_LOCAL_FILESYSTEM_");
|
||
|
}
|
||
|
|
||
|
string[] listAllOrigins(string dir = ".") {
|
||
|
import std.array;
|
||
|
auto app = appender!(string[]);
|
||
|
foreach (DirEntry entry; dirEntries(dir, SpanMode.shallow, false)) {
|
||
|
string origin = originFromDbPath(entry.name);
|
||
|
if (origin !is null) {
|
||
|
app ~= origin;
|
||
|
}
|
||
|
}
|
||
|
return app[];
|
||
|
}
|