Module Database EnableExplicit UseSQLiteDatabase() ; Private constants #DB = 0 ; Private procedures Procedure.s SHA256(text.s) Protected *Buf, Len.i, Hash.s Len = StringByteLength(text, #PB_UTF8) If Len = 0 : ProcedureReturn "" : EndIf *Buf = AllocateMemory(Len) If *Buf PokeS(*Buf, text, -1, #PB_UTF8 | #PB_String_NoZero) Hash = LCase(Fingerprint(*Buf, Len, #PB_Cipher_SHA2, 256)) FreeMemory(*Buf) EndIf ProcedureReturn Hash EndProcedure Procedure.s GenerateHex(Bytes.i) Protected Token.s, i For i = 1 To Bytes Token + RSet(Hex(Random(255)), 2, "0") Next ProcedureReturn LCase(Token) EndProcedure Procedure.s JSONEscape(s.s) s = ReplaceString(s, "\", "\\") s = ReplaceString(s, ~"\"", ~"\\\"") s = ReplaceString(s, ~"\n", ~"\\n") s = ReplaceString(s, ~"\r", ~"\\r") s = ReplaceString(s, ~"\t", ~"\\t") ProcedureReturn s EndProcedure ;- Public procedures Procedure Init(Path.s) Protected TempFile.i If FileSize(Path) = -1 TempFile = CreateFile(#PB_Any, Path) CloseFile(TempFile) EndIf If Not OpenDatabase(#DB, Path, "", "", #PB_Database_SQLite) ProcedureReturn #False EndIf DatabaseUpdate(#DB, "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL COLLATE NOCASE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at INTEGER NOT NULL)") DatabaseUpdate(#DB, "CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, user_id INTEGER NOT NULL, username TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL)") DatabaseUpdate(#DB, "CREATE TABLE IF NOT EXISTS installed_apps (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, app_id TEXT NOT NULL, manifest TEXT NOT NULL, permissions TEXT NOT NULL, installed_at INTEGER NOT NULL, UNIQUE(user_id, app_id))") ProcedureReturn #True EndProcedure Procedure Close() If IsDatabase(#DB) CloseDatabase(#DB) EndIf EndProcedure ; User management Procedure UserCount() Protected Count = 0 If DatabaseQuery(#DB, "SELECT COUNT(*) FROM users") If NextDatabaseRow(#DB) : Count = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn Count EndProcedure Procedure FindUser(Username.s) Protected ID = 0 SetDatabaseString(#DB, 0, Username) If DatabaseQuery(#DB, "SELECT id FROM users WHERE username = ? COLLATE NOCASE") If NextDatabaseRow(#DB) : ID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn ID EndProcedure Procedure CreateUser(Username.s, Password.s) Protected Salt.s = GenerateHex(16) Protected Hash.s = SHA256(Password + Salt) SetDatabaseString(#DB, 0, Username) SetDatabaseString(#DB, 1, Hash) SetDatabaseString(#DB, 2, Salt) SetDatabaseLong(#DB, 3, Date()) ProcedureReturn DatabaseUpdate(#DB, "INSERT INTO users (username, password_hash, salt, created_at) VALUES (?, ?, ?, ?)") EndProcedure Procedure ChangePassword(UserID.i, NewPassword.s) Protected Salt.s = GenerateHex(16) Protected Hash.s = SHA256(NewPassword + Salt) SetDatabaseString(#DB, 0, Hash) SetDatabaseString(#DB, 1, Salt) SetDatabaseLong(#DB, 2, UserID) DatabaseUpdate(#DB, "UPDATE users SET password_hash = ?, salt = ? WHERE id = ?") EndProcedure Procedure.s ValidateCredentials(Username.s, Password.s) Protected StoredUser.s, Hash.s, Salt.s SetDatabaseString(#DB, 0, Username) If DatabaseQuery(#DB, "SELECT username, password_hash, salt FROM users WHERE username = ? COLLATE NOCASE") If NextDatabaseRow(#DB) StoredUser = GetDatabaseString(#DB, 0) Hash = GetDatabaseString(#DB, 1) Salt = GetDatabaseString(#DB, 2) EndIf FinishDatabaseQuery(#DB) EndIf If StoredUser = "" : ProcedureReturn "" : EndIf If SHA256(Password + Salt) = Hash : ProcedureReturn StoredUser : EndIf ProcedureReturn "" EndProcedure Procedure.s CreateSession(UserID.i, Username.s) Protected Token.s = GenerateHex(32) Protected Now.i = Date() SetDatabaseString(#DB, 0, Token) SetDatabaseLong(#DB, 1, UserID) SetDatabaseString(#DB, 2, Username) SetDatabaseLong(#DB, 3, Now) SetDatabaseLong(#DB, 4, Now + #SESSION_DURATION) DatabaseUpdate(#DB, "INSERT OR REPLACE INTO sessions (token, user_id, username, created_at, expires_at) VALUES (?, ?, ?, ?, ?)") ProcedureReturn Token EndProcedure Procedure.s ValidateSession(Token.s) Protected Username.s = "" If Len(Token) <> 64 : ProcedureReturn "" : EndIf SetDatabaseString(#DB, 0, Token) SetDatabaseLong(#DB, 1, Date()) If DatabaseQuery(#DB, "SELECT username FROM sessions WHERE token = ? AND expires_at > ?") If NextDatabaseRow(#DB) : Username = GetDatabaseString(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn Username EndProcedure Procedure DeleteSession(Token.s) SetDatabaseString(#DB, 0, Token) DatabaseUpdate(#DB, "DELETE FROM sessions WHERE token = ?") EndProcedure Procedure CleanExpiredSessions() SetDatabaseLong(#DB, 0, Date()) DatabaseUpdate(#DB, "DELETE FROM sessions WHERE expires_at <= ?") EndProcedure ;- File system Procedure.i FSInit() Protected Now.i = Date() DatabaseUpdate(#DB, "CREATE TABLE IF NOT EXISTS fs_nodes (id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id INTEGER NOT NULL, parent_id INTEGER, name TEXT NOT NULL COLLATE NOCASE, is_dir INTEGER NOT NULL DEFAULT 0, mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', size INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, modified_at INTEGER NOT NULL)") DatabaseUpdate(#DB, "CREATE UNIQUE INDEX IF NOT EXISTS idx_fs_unique ON fs_nodes(parent_id, name)") DatabaseUpdate(#DB, "CREATE INDEX IF NOT EXISTS idx_fs_owner ON fs_nodes(owner_id)") ; Shared root - owner_id 0, one row, idempotent SetDatabaseLong(#DB, 0, Now) SetDatabaseLong(#DB, 1, Now) DatabaseUpdate(#DB, "INSERT OR IGNORE INTO fs_nodes (owner_id, parent_id, name, is_dir, created_at, modified_at) VALUES (0, NULL, 'shared', 1, ?, ?)") ProcedureReturn #True EndProcedure Procedure.i FSGetOrCreateHome(UserID.i) Protected ID.i = 0, Now.i SetDatabaseLong(#DB, 0, UserID) If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE owner_id = ? AND parent_id IS NULL") If NextDatabaseRow(#DB) : ID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf If ID = 0 Now = Date() SetDatabaseLong(#DB, 0, UserID) SetDatabaseLong(#DB, 1, Now) SetDatabaseLong(#DB, 2, Now) DatabaseUpdate(#DB, "INSERT INTO fs_nodes (owner_id, parent_id, name, is_dir, created_at, modified_at) VALUES (?, NULL, 'home', 1, ?, ?)") If DatabaseQuery(#DB, "SELECT last_insert_rowid()") If NextDatabaseRow(#DB) : ID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf EndIf ProcedureReturn ID EndProcedure Procedure.i FSResolve(UserID.i, Path.s) Protected CurrentID.i, SegCount.i, i.i, Seg.s, CandID.i While Left(Path, 1) = "/" : Path = Mid(Path, 2) : Wend While Right(Path, 1) = "/" : Path = Left(Path, Len(Path) - 1) : Wend If Path = "shared" Or Left(Path, 7) = "shared/" If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE owner_id = 0 AND parent_id IS NULL") If NextDatabaseRow(#DB) : CurrentID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf If Path = "shared" : ProcedureReturn CurrentID : EndIf Path = Mid(Path, 8) ; strip "shared/" Else CurrentID = FSGetOrCreateHome(UserID) If Path = "" : ProcedureReturn CurrentID : EndIf EndIf If CurrentID = 0 : ProcedureReturn 0 : EndIf SegCount = CountString(Path, "/") + 1 For i = 1 To SegCount Seg = StringField(Path, i, "/") If Seg = "" : Continue : EndIf CandID = 0 SetDatabaseLong(#DB, 0, CurrentID) SetDatabaseString(#DB, 1, Seg) If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE parent_id = ? AND name = ?") If NextDatabaseRow(#DB) : CandID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf If CandID = 0 : ProcedureReturn 0 : EndIf CurrentID = CandID Next ProcedureReturn CurrentID EndProcedure Procedure.s FSList(NodeID.i) Protected JSON.s = "[", First.i = #True SetDatabaseLong(#DB, 0, NodeID) If DatabaseQuery(#DB, "SELECT id, name, is_dir, mime_type, size, modified_at FROM fs_nodes WHERE parent_id = ? ORDER BY is_dir DESC, name") While NextDatabaseRow(#DB) If Not First : JSON + "," : EndIf JSON + ~"{\"id\":" + GetDatabaseLong(#DB, 0) + ~",\"name\":\"" + JSONEscape(GetDatabaseString(#DB, 1)) + ~"\"" + ~",\"is_dir\":" + GetDatabaseLong(#DB, 2) + ~",\"mime_type\":\"" + JSONEscape(GetDatabaseString(#DB, 3)) + ~"\"" + ~",\"size\":" + GetDatabaseLong(#DB, 4) + ~",\"modified_at\":" + GetDatabaseLong(#DB, 5) + "}" First = #False Wend FinishDatabaseQuery(#DB) EndIf ProcedureReturn JSON + "]" EndProcedure Procedure.s FSStat(NodeID.i) Protected JSON.s = "" SetDatabaseLong(#DB, 0, NodeID) If DatabaseQuery(#DB, "SELECT id, owner_id, parent_id, name, is_dir, mime_type, size, created_at, modified_at FROM fs_nodes WHERE id = ?") If NextDatabaseRow(#DB) JSON = ~"{\"id\":" + GetDatabaseLong(#DB, 0) + ~",\"owner_id\":" + GetDatabaseLong(#DB, 1) + ~",\"parent_id\":" + GetDatabaseLong(#DB, 2) + ~",\"name\":\"" + JSONEscape(GetDatabaseString(#DB, 3)) + ~"\"" + ~",\"is_dir\":" + GetDatabaseLong(#DB, 4) + ~",\"mime_type\":\"" + JSONEscape(GetDatabaseString(#DB, 5)) + ~"\"" + ~",\"size\":" + GetDatabaseLong(#DB, 6) + ~",\"created_at\":" + GetDatabaseLong(#DB, 7) + ~",\"modified_at\":" + GetDatabaseLong(#DB, 8) + "}" EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn JSON EndProcedure Procedure.i FSGetOwner(NodeID.i, *IsDir.Integer = 0) Protected OwnerID.i = -1 SetDatabaseLong(#DB, 0, NodeID) If DatabaseQuery(#DB, "SELECT owner_id, is_dir FROM fs_nodes WHERE id = ?") If NextDatabaseRow(#DB) OwnerID = GetDatabaseLong(#DB, 0) If *IsDir : *IsDir\i = GetDatabaseLong(#DB, 1) : EndIf EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn OwnerID EndProcedure Procedure.i FSMkdir(UserID.i, ParentID.i, Name.s) Protected Now.i = Date(), ID.i = 0 SetDatabaseLong(#DB, 0, UserID) SetDatabaseLong(#DB, 1, ParentID) SetDatabaseString(#DB, 2, Name) SetDatabaseLong(#DB, 3, Now) SetDatabaseLong(#DB, 4, Now) DatabaseUpdate(#DB, "INSERT OR IGNORE INTO fs_nodes (owner_id, parent_id, name, is_dir, created_at, modified_at) VALUES (?, ?, ?, 1, ?, ?)") SetDatabaseLong(#DB, 0, ParentID) SetDatabaseString(#DB, 1, Name) If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE parent_id = ? AND name = ?") If NextDatabaseRow(#DB) : ID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn ID EndProcedure Procedure.i FSCreateFile(UserID.i, ParentID.i, Name.s, MimeType.s) Protected Now.i = Date(), ID.i = 0 SetDatabaseLong(#DB, 0, UserID) SetDatabaseLong(#DB, 1, ParentID) SetDatabaseString(#DB, 2, Name) SetDatabaseString(#DB, 3, MimeType) SetDatabaseLong(#DB, 4, Now) SetDatabaseLong(#DB, 5, Now) DatabaseUpdate(#DB, "INSERT OR IGNORE INTO fs_nodes (owner_id, parent_id, name, is_dir, mime_type, created_at, modified_at) VALUES (?, ?, ?, 0, ?, ?, ?)") SetDatabaseLong(#DB, 0, ParentID) SetDatabaseString(#DB, 1, Name) If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE parent_id = ? AND name = ?") If NextDatabaseRow(#DB) : ID = GetDatabaseLong(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn ID EndProcedure Procedure FSUpdateFile(NodeID.i, Size.i) SetDatabaseLong(#DB, 0, Size) SetDatabaseLong(#DB, 1, Date()) SetDatabaseLong(#DB, 2, NodeID) DatabaseUpdate(#DB, "UPDATE fs_nodes SET size = ?, modified_at = ? WHERE id = ?") EndProcedure Procedure FSDelete(NodeID.i) Protected NewList ChildIDs.i() SetDatabaseLong(#DB, 0, NodeID) If DatabaseQuery(#DB, "SELECT id FROM fs_nodes WHERE parent_id = ?") While NextDatabaseRow(#DB) AddElement(ChildIDs()) : ChildIDs() = GetDatabaseLong(#DB, 0) Wend FinishDatabaseQuery(#DB) EndIf ForEach ChildIDs() FSDelete(ChildIDs()) Next SetDatabaseLong(#DB, 0, NodeID) DatabaseUpdate(#DB, "DELETE FROM fs_nodes WHERE id = ?") EndProcedure Procedure FSMove(NodeID.i, NewParentID.i, NewName.s) SetDatabaseLong(#DB, 0, NewParentID) SetDatabaseString(#DB, 1, NewName) SetDatabaseLong(#DB, 2, Date()) SetDatabaseLong(#DB, 3, NodeID) DatabaseUpdate(#DB, "UPDATE fs_nodes SET parent_id = ?, name = ?, modified_at = ? WHERE id = ?") EndProcedure ;- Applications Procedure.i AppInstall(UserID.i, AppID.s, Manifest.s, Permissions.s) SetDatabaseLong(#DB, 0, UserID) SetDatabaseString(#DB, 1, AppID) SetDatabaseString(#DB, 2, Manifest) SetDatabaseString(#DB, 3, Permissions) SetDatabaseLong(#DB, 4, Date()) ProcedureReturn DatabaseUpdate(#DB, "INSERT OR REPLACE INTO installed_apps (user_id, app_id, manifest, permissions, installed_at) VALUES (?, ?, ?, ?, ?)") EndProcedure Procedure AppUninstall(UserID.i, AppID.s) SetDatabaseLong(#DB, 0, UserID) SetDatabaseString(#DB, 1, AppID) DatabaseUpdate(#DB, "DELETE FROM installed_apps WHERE user_id = ? AND app_id = ?") EndProcedure Procedure.i AppExists(UserID.i, AppID.s) Protected Exists.i = #False SetDatabaseLong(#DB, 0, UserID) SetDatabaseString(#DB, 1, AppID) If DatabaseQuery(#DB, "SELECT id FROM installed_apps WHERE user_id = ? AND app_id = ?") Exists = Bool(NextDatabaseRow(#DB)) FinishDatabaseQuery(#DB) EndIf ProcedureReturn Exists EndProcedure Procedure.s AppGetPermissions(UserID.i, AppID.s) Protected Result.s = "" SetDatabaseLong(#DB, 0, UserID) SetDatabaseString(#DB, 1, AppID) If DatabaseQuery(#DB, "SELECT permissions FROM installed_apps WHERE user_id = ? AND app_id = ?") If NextDatabaseRow(#DB) : Result = GetDatabaseString(#DB, 0) : EndIf FinishDatabaseQuery(#DB) EndIf ProcedureReturn Result EndProcedure Procedure.s AppList(UserID.i) Protected JSON.s = "[", First.i = #True Protected AppID.s, Manifest.s, Perms.s, InstalledAt.i SetDatabaseLong(#DB, 0, UserID) If Not DatabaseQuery(#DB, "SELECT app_id, manifest, permissions, installed_at FROM installed_apps WHERE user_id = ? ORDER BY installed_at ASC") ProcedureReturn "[]" EndIf While NextDatabaseRow(#DB) AppID = GetDatabaseString(#DB, 0) Manifest = GetDatabaseString(#DB, 1) Perms = GetDatabaseString(#DB, 2) InstalledAt = GetDatabaseLong(#DB, 3) If Not First : JSON + "," : EndIf First = #False ; Manifest and permissions are inlined as-is (already valid JSON) JSON + ~"{\"app_id\":\"" + JSONEscape(AppID) + ~"\"" + ~",\"manifest\":" + Manifest + ~",\"permissions\":" + Perms + ~",\"installed_at\":" + InstalledAt + "}" Wend FinishDatabaseQuery(#DB) ProcedureReturn JSON + "]" EndProcedure EndModule ; IDE Options = PureBasic 6.30 (Windows - x64) ; CursorPosition = 37 ; Folding = BAAAA+ ; EnableXP ; DPIAware