module profile.data_impl_sqlite; import slf4d; import handy_httpd.components.optional; import handy_httpd.components.handler : HttpStatusException; import handy_httpd.components.response : HttpStatus; import d2sqlite3; import profile.data; import profile.model; import util.sqlite; const DEFAULT_USERS_DIR = "users"; /// Profile repository that uses an SQLite3 database file for each profile. class FileSystemProfileRepository : ProfileRepository { import std.path; import std.file; private const string usersDir; private const string username; this(string usersDir, string username) { this.usersDir = usersDir; this.username = username; } this(string username) { this(DEFAULT_USERS_DIR, username); } Optional!Profile findByName(string name) { string path = getProfilePath(name); if (!exists(path)) return Optional!Profile.empty; return Optional!Profile.of(new Profile(name)); } Profile createProfile(string name) { string path = getProfilePath(name); if (exists(path)) throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Profile already exists."); if (!exists(getProfilesDir())) mkdir(getProfilesDir()); ProfileDataSource ds = new SqliteProfileDataSource(path); import std.datetime; auto propsRepo = ds.getPropertiesRepository(); propsRepo.setProperty("name", name); propsRepo.setProperty("createdAt", Clock.currTime(UTC()).toISOExtString()); propsRepo.setProperty("user", username); return new Profile(name); } Profile[] findAll() { string profilesDir = getProfilesDir(); if (!exists(profilesDir)) return []; Profile[] profiles; foreach (DirEntry entry; dirEntries(profilesDir, SpanMode.shallow, false)) { import std.string : endsWith; const suffix = ".sqlite"; if (endsWith(entry.name, suffix)) { string profileName = baseName(entry.name, suffix); profiles ~= new Profile(profileName); } } import std.algorithm.sorting : sort; sort(profiles); return profiles; } void deleteByName(string name) { string path = getProfilePath(name); if (exists(path)) { std.file.remove(path); } } ProfileDataSource getDataSource(in Profile profile) { return new SqliteProfileDataSource(getProfilePath(profile.name)); } private string getProfilesDir() { return buildPath(this.usersDir, username, "profiles"); } private string getProfilePath(string name) { return buildPath(this.usersDir, username, "profiles", name ~ ".sqlite"); } } class SqlitePropertiesRepository : PropertiesRepository { private Database db; this(Database db) { this.db = db; } Optional!string findProperty(string propertyName) { return findOne( db, "SELECT value FROM profile_property WHERE property = ?", r => r.peek!string(0), propertyName ); } void setProperty(string name, string value) { if (findProperty(name).isNull) { Statement stmt = this.db.prepare("INSERT INTO profile_property (property, value) VALUES (?, ?)"); stmt.bind(1, name); stmt.bind(2, value); stmt.execute(); } else { Statement stmt = this.db.prepare("UPDATE profile_property SET value = ? WHERE property = ?"); stmt.bind(1, value); stmt.bind(2, name); stmt.execute(); } } void deleteProperty(string name) { Statement stmt = this.db.prepare("DELETE FROM profile_property WHERE property = ?"); stmt.bind(1, name); stmt.execute(); } ProfileProperty[] findAll() { Statement stmt = this.db.prepare("SELECT * FROM profile_property ORDER BY property ASC"); ResultRange result = stmt.execute(); ProfileProperty[] props; foreach (Row row; result) { ProfileProperty prop; prop.property = row.peek!string("property"); prop.value = row.peek!string("value"); props ~= prop; } return props; } } /** * An SQLite implementation of the ProfileDataSource that uses a single * database connection to initialize various entity data access objects lazily. */ class SqliteProfileDataSource : ProfileDataSource { import account.data; import account.data_impl_sqlite; const SCHEMA = import("schema.sql"); private const string dbPath; private Database db; this(string path) { this.dbPath = path; import std.file : exists; bool needsInit = !exists(path); this.db = Database(path); if (needsInit) { infoF!"Initializing database: %s"(dbPath); db.run(SCHEMA); } } PropertiesRepository getPropertiesRepository() { return new SqlitePropertiesRepository(db); } AccountRepository getAccountRepository() { return new SqliteAccountRepository(db); } }