[niepce: 14/29] engine+rust: implement Library in Rust.



commit 4774d73edd8241fba509dfc052082da0a1320605
Author: Hubert Figuière <hub figuiere net>
Date:   Wed Jun 21 21:51:35 2017 -0400

    engine+rust: implement Library in Rust.

 Cargo.toml                 |    4 +-
 src/engine/db/keyword.rs   |   17 ++
 src/engine/db/libfile.rs   |   65 ++++++-
 src/engine/db/libfolder.rs |   45 ++++-
 src/engine/db/library.rs   |  533 ++++++++++++++++++++++++++++++++++++++++++++
 src/engine/db/mod.rs       |   11 +
 src/fwk/base/date.cpp      |    2 +-
 src/fwk/base/date.rs       |  148 ++++++++++++
 src/fwk/base/mod.rs        |    1 +
 src/fwk/mod.rs             |    2 +
 src/fwk/utils/exempi.rs    |   10 +-
 src/lib.rs                 |    1 +
 src/niepce/Makefile.am     |    1 +
 13 files changed, 829 insertions(+), 11 deletions(-)
---
diff --git a/Cargo.toml b/Cargo.toml
index 43a3478..77c714f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,8 +5,8 @@ authors = ["Hubert Figuière <hub figuiere net>"]
 
 [dependencies]
 libc = "0.2.23"
-sqlite = "0.23.4"
-exempi = "2.4.1"
+rusqlite = "0.12.0"
+exempi = "2.4.3"
 glib-sys = { git = "https://github.com/gtk-rs/sys"; }
 gio-sys = { git = "https://github.com/gtk-rs/sys"; }
 gio = { git = "https://github.com/gtk-rs/gio"; }
diff --git a/src/engine/db/keyword.rs b/src/engine/db/keyword.rs
index 2d55e7e..2739a18 100644
--- a/src/engine/db/keyword.rs
+++ b/src/engine/db/keyword.rs
@@ -18,9 +18,11 @@
  */
 
 use super::LibraryId;
+use super::FromDb;
 use libc::c_char;
 use std::ffi::CStr;
 use std::ffi::CString;
+use rusqlite;
 
 pub struct Keyword {
     id: LibraryId,
@@ -45,6 +47,21 @@ impl Keyword {
     }
 }
 
+impl FromDb for Keyword {
+    fn read_db_columns() -> &'static str {
+        "id,keyword"
+    }
+
+    fn read_db_tables() -> &'static str {
+        "keywords"
+    }
+
+    fn read_from(row: &rusqlite::Row) -> Self {
+        let kw : String = row.get(1);
+        Keyword::new(row.get(0), &kw)
+    }
+}
+
 #[no_mangle]
 pub extern fn engine_db_keyword_new(id: i64, keyword: *const c_char) -> *mut Keyword {
     let kw = Box::new(Keyword::new(id, &*unsafe { CStr::from_ptr(keyword) }.to_string_lossy()));
diff --git a/src/engine/db/libfile.rs b/src/engine/db/libfile.rs
index 0bf968f..c42a9c7 100644
--- a/src/engine/db/libfile.rs
+++ b/src/engine/db/libfile.rs
@@ -22,15 +22,18 @@ use std::ffi::CStr;
 use std::ffi::CString;
 use std::mem::transmute;
 use std::path::{Path, PathBuf};
+use rusqlite;
 
+use super::FromDb;
 use super::LibraryId;
 use super::fsfile::FsFile;
 use fwk::base::PropertyIndex;
 use root::eng::NiepceProperties as Np;
+use fwk;
 
 #[repr(i32)]
 #[allow(non_camel_case_types)]
-#[derive(Clone)]
+#[derive(Clone,PartialEq)]
 pub enum FileType {
     UNKNOWN = 0,
     RAW = 1,
@@ -39,6 +42,19 @@ pub enum FileType {
     VIDEO = 4
 }
 
+impl From<i32> for FileType {
+    fn from(t: i32) -> Self {
+        match t {
+            0 => FileType::UNKNOWN,
+            1 => FileType::RAW,
+            2 => FileType::RAW_JPEG,
+            3 => FileType::IMAGE,
+            4 => FileType::VIDEO,
+            _ => FileType::UNKNOWN,
+        }
+    }
+}
+
 pub struct LibFile {
     id: LibraryId,
     folder_id: LibraryId,
@@ -156,6 +172,53 @@ impl LibFile {
     }
 }
 
+impl FromDb for LibFile {
+    fn read_db_columns() -> &'static str {
+        "files.id,parent_id,fsfiles.path,\
+         name,orientation,rating,label,file_type,fsfiles.id,flag"
+    }
+
+    fn read_db_tables() -> &'static str {
+        "files, fsfiles"
+    }
+
+    fn read_from(row: &rusqlite::Row) -> Self {
+        //DBG_ASSERT(dbdrv->get_number_of_columns() == 10, "wrong number of columns");
+        let id = row.get(0);
+        let fid = row.get(1);
+        let path: String = row.get(2);
+        let name: String = row.get(3);
+        let fsfid = row.get(8);
+        let mut file = LibFile::new(id, fid, fsfid, PathBuf::from(&path), &name);
+
+        file.set_orientation(row.get(4));
+        file.set_rating(row.get(5));
+        file.set_label(row.get(6));
+        file.set_flag(row.get(9));
+        let file_type: i32 = row.get(7);
+        file.set_file_type(FileType::from(file_type));
+
+        file
+    }
+}
+
+/**
+ * Converts a mimetype, which is expensive to calculate, into a FileType.
+ * @param mime The mimetype we want to know as a filetype
+ * @return the filetype
+ * @todo: add the JPEG+RAW file types.
+ */
+pub fn mimetype_to_filetype(mime: &fwk::MimeType) -> FileType {
+    if mime.is_digicam_raw() {
+        return FileType::RAW;
+    } else if mime.is_image() {
+        return FileType::IMAGE;
+    } else if mime.is_movie() {
+        return FileType::VIDEO;
+    }
+    FileType::UNKNOWN
+}
+
 #[no_mangle]
 pub extern fn engine_db_libfile_new(id: LibraryId, folder_id: LibraryId,
                                     fs_file_id: LibraryId, path: *const c_char,
diff --git a/src/engine/db/libfolder.rs b/src/engine/db/libfolder.rs
index 9a7a124..824bef2 100644
--- a/src/engine/db/libfolder.rs
+++ b/src/engine/db/libfolder.rs
@@ -20,7 +20,9 @@
 use libc::c_char;
 use std::ffi::CStr;
 use std::ffi::CString;
+use rusqlite;
 
+use super::FromDb;
 use super::LibraryId;
 
 #[repr(i32)]
@@ -30,6 +32,16 @@ pub enum VirtualType {
     TRASH = 1
 }
 
+impl From<i32> for VirtualType {
+    fn from(t: i32) -> Self {
+        match t {
+            0 => VirtualType::NONE,
+            1 => VirtualType::TRASH,
+            _ => VirtualType::NONE,
+        }
+    }
+}
+
 pub struct LibFolder {
     id: LibraryId,
     name: String,
@@ -79,6 +91,33 @@ impl LibFolder {
     pub fn set_virtual_type(&mut self, virt: VirtualType) {
         self.virt = virt;
     }
+
+}
+
+impl FromDb for LibFolder {
+
+    fn read_db_columns() -> &'static str {
+        "id,name,virtual,locked,expanded"
+    }
+
+    fn read_db_tables() -> &'static str {
+        "folders"
+    }
+
+    fn read_from(row: &rusqlite::Row) -> Self {
+        let id: LibraryId = row.get(0);
+        let name: String = row.get(1);
+        let virt_type: i32 = row.get(2);
+        let locked = row.get(3);
+        let expanded = row.get(4);
+
+        let mut libfolder = LibFolder::new(id, &name);
+        libfolder.set_virtual_type(VirtualType::from(virt_type));
+        libfolder.set_locked(locked);
+        libfolder.set_expanded(expanded);
+
+        libfolder
+    }
 }
 
 #[no_mangle]
@@ -125,9 +164,5 @@ pub extern fn engine_db_libfolder_set_expanded(this: &mut LibFolder, expanded: b
 
 #[no_mangle]
 pub extern fn engine_db_libfolder_set_virtual_type(this: &mut LibFolder, t: i32) {
-    this.set_virtual_type(match t {
-        0 => VirtualType::NONE,
-        1 => VirtualType::TRASH,
-        _ => VirtualType::NONE,
-    });
+    this.set_virtual_type(VirtualType::from(t));
 }
diff --git a/src/engine/db/library.rs b/src/engine/db/library.rs
new file mode 100644
index 0000000..0b78bb7
--- /dev/null
+++ b/src/engine/db/library.rs
@@ -0,0 +1,533 @@
+/*
+ * niepce - eng/db/library.rs
+ *
+ * Copyright (C) 2017 Hubert Figuière
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use std::path::{
+    Path,
+    PathBuf
+};
+use rusqlite;
+use super::{
+    FromDb,
+    LibraryId
+};
+use super::libfolder;
+use super::libfolder::LibFolder;
+use super::libfile;
+use super::libfile::LibFile;
+use super::keyword::Keyword;
+
+use fwk;
+
+const DB_SCHEMA_VERSION: i32 = 6;
+const DATABASENAME: &str = "niepcelibrary.db";
+
+enum NotificationCenter {}
+
+#[repr(i32)]
+pub enum Managed {
+    NO = 0,
+    YES = 1
+}
+
+pub struct Library {
+    maindir: PathBuf,
+    dbpath: PathBuf,
+    dbconn: Option<rusqlite::Connection>,
+    inited: bool
+}
+
+impl Library {
+
+    pub fn new(dir: &Path) -> Library {
+        let mut dbpath = PathBuf::from(dir);
+        dbpath.push(DATABASENAME);
+        let mut lib = Library {
+            maindir: PathBuf::from(dir),
+            dbpath: dbpath,
+            dbconn: None,
+            inited: false
+        };
+
+        lib.inited = lib.init();
+
+        lib
+    }
+
+    fn init(&mut self) -> bool {
+        let conn_attempt = rusqlite::Connection::open(self.dbpath.clone());
+        if let Ok(conn) = conn_attempt {
+            self.dbconn = Some(conn);
+        } else {
+            return false;
+        }
+
+        let version = self.check_database_version();
+        if version == -1 {
+            // error
+        } else if version == 0 {
+            // version == 0
+            return self.init_db();
+        } else if version != DB_SCHEMA_VERSION {
+            // WAT?
+        }
+        true
+    }
+
+    pub fn dbpath(&self) -> &Path {
+        &self.dbpath
+    }
+
+    pub fn is_ok(&self) -> bool {
+        self.inited
+    }
+
+    fn check_database_version(&self) -> i32 {
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(mut stmt) = conn.prepare("SELECT value FROM admin WHERE key='version'") {
+                let mut rows = stmt.query(&[]).unwrap();
+                if let Some(Ok(row)) = rows.next() {
+                    let value: String = row.get(0);
+                    if let Ok(v) = i32::from_str_radix(&value, 10) {
+                        return v;
+                    } else {
+                        return -1;
+                    }
+                }
+            } else {
+                // if query fail we assume 0 to create the database.
+                return 0;
+            }
+        }
+
+        -1
+    }
+
+    fn init_db(&mut self) -> bool {
+        if let Some(ref conn) = self.dbconn {
+            conn.execute("CREATE TABLE admin (key TEXT NOT NULL, value TEXT)", &[]).unwrap();
+            conn.execute("INSERT INTO admin (key, value) \
+                          VALUES ('version', ?1)", &[&DB_SCHEMA_VERSION]).unwrap();
+            conn.execute("CREATE TABLE vaults (id INTEGER PRIMARY KEY, path TEXT)", &[]).unwrap();
+            conn.execute("CREATE TABLE folders (id INTEGER PRIMARY KEY,\
+                          path TEXT, name TEXT, \
+                          vault_id INTEGER DEFAULT 0, \
+                          locked INTEGER DEFAULT 0, \
+                          virtual INTEGER DEFAULT 0, \
+                          expanded INTEGER DEFAULT 0, \
+                          parent_id INTEGER)", &[]).unwrap();
+            let trash_type = libfolder::VirtualType::TRASH as i32;
+            conn.execute("insert into folders (name, locked, virtual, parent_id) \
+                          values (:1, 1, :2, 0)",
+                         &[&"Trash", &trash_type]).unwrap();
+
+            conn.execute("CREATE TABLE files (id INTEGER PRIMARY KEY,\
+                          main_file INTEGER, name TEXT, parent_id INTEGER,\
+                          orientation INTEGER, file_type INTEGER,\
+                          file_date INTEGER, rating INTEGER DEFAULT 0, \
+                          label INTEGER, flag INTEGER DEFAULT 0, \
+                          import_date INTEGER, mod_date INTEGER, \
+                          xmp TEXT, xmp_date INTEGER, xmp_file INTEGER,\
+                          jpeg_file INTEGER)", &[]).unwrap();
+            conn.execute("CREATE TABLE fsfiles (id INTEGER PRIMARY KEY,\
+                          path TEXT)", &[]).unwrap();
+            conn.execute("CREATE TABLE keywords (id INTEGER PRIMARY KEY,\
+                          keyword TEXT, parent_id INTEGER DEFAULT 0)",
+                         &[]).unwrap();
+            conn.execute("CREATE TABLE keywording (file_id INTEGER,\
+                          keyword_id INTEGER, UNIQUE(file_id, keyword_id))",
+                         &[]).unwrap();
+            conn.execute("CREATE TABLE labels (id INTEGER PRIMARY KEY,\
+                          name TEXT, color TEXT)", &[]).unwrap();
+            conn.execute("CREATE TABLE xmp_update_queue (id INTEGER UNIQUE)",
+                         &[]).unwrap();
+            conn.execute("CREATE TRIGGER file_update_trigger UPDATE ON files \
+                          BEGIN \
+                          UPDATE files SET mod_date = strftime('%s','now');\
+                          END", &[]).unwrap();
+            conn.execute("CREATE TRIGGER xmp_update_trigger UPDATE OF xmp ON files \
+                          BEGIN \
+                          INSERT OR IGNORE INTO xmp_update_queue (id) VALUES(new.id);\
+                          SELECT rewrite_xmp();\
+                          END", &[]).unwrap();
+
+            //XXX            self.notify();
+            return true;
+        }
+        false
+    }
+
+    fn leaf_name_for_pathname(pathname: &str) -> Option<String> {
+        let path = Path::new(pathname);
+        if let Some(ref name) = path.file_name() {
+            if let Some(s) = name.to_str() {
+                return Some(String::from(s));
+            }
+        }
+        None
+    }
+
+    fn get_content(&self, id: LibraryId, sql_where: &str) -> Vec<LibFile> {
+        if let Some(ref conn) = self.dbconn {
+            let sql = format!("SELECT {} FROM {} \
+                               WHERE {} \
+                               AND files.main_file=fsfiles.id",
+                              LibFile::read_db_columns(),
+                              LibFile::read_db_tables(),
+                              sql_where);
+            if let Ok(mut stmt) = conn.prepare(&sql) {
+                let mut files: Vec<LibFile> = vec!();
+                let mut rows = stmt.query(&[&id]).unwrap();
+                while  let Some(Ok(row)) = rows.next() {
+                    let libfile = LibFile::read_from(&row);
+                    files.push(libfile);
+                }
+                return files;
+            }
+        }
+
+        vec!()
+
+    }
+
+    pub fn add_folder(&self, folder: &str) -> Option<LibFolder> {
+        if let Some(foldername) = Self::leaf_name_for_pathname(folder) {
+            if let Some(ref conn) = self.dbconn {
+                if let Ok(c) = conn.execute(
+                    "INSERT INTO folders (path,name,vault_id,parent_id) VALUES(:1, :2, '0', '0')",
+                    &[&folder, &foldername]) {
+                    if c != 1 {
+                        return None;
+                    }
+                    let id = conn.last_insert_rowid();
+                    // DBG_OUT("last row inserted %Ld", (long long)id);
+                    return Some(LibFolder::new(id, &foldername));
+                }
+            }
+        }
+        None
+    }
+
+    pub fn get_folder(&self, folder: &str) -> Option<LibFolder> {
+        if let Some(foldername) = Self::leaf_name_for_pathname(folder) {
+            if let Some(ref conn) = self.dbconn {
+                let sql = format!("SELECT {} FROM {} WHERE path=:1",
+                                  LibFolder::read_db_columns(),
+                                  LibFolder::read_db_tables());
+                if let Ok(mut stmt) = conn.prepare(&sql) {
+                    let mut rows = stmt.query(&[&foldername]).unwrap();
+                    if let Some(Ok(row)) = rows.next() {
+                        let libfolder = LibFolder::read_from(&row);
+                        return Some(libfolder);
+                    }
+                }
+            }
+        }
+        None
+    }
+
+    pub fn get_all_folders(&self) -> Vec<LibFolder> {
+        if let Some(ref conn) = self.dbconn {
+            let sql = format!("SELECT {} FROM {}",
+                              LibFolder::read_db_columns(),
+                              LibFolder::read_db_tables());
+            if let Ok(mut stmt) = conn.prepare(&sql) {
+                let mut folders: Vec<LibFolder> = vec!();
+                let mut rows = stmt.query(&[]).unwrap();
+                while  let Some(Ok(row)) = rows.next() {
+                    let libfolder = LibFolder::read_from(&row);
+                    folders.push(libfolder);
+                }
+                return folders;
+            }
+        }
+        vec!()
+    }
+
+    pub fn get_folder_content(&self, folder_id: LibraryId) -> Vec<LibFile> {
+        self.get_content(folder_id, "parent_id = :1")
+    }
+
+    pub fn count_folder(&self, folder_id: LibraryId) -> i64 {
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(mut stmt) = conn.prepare("SELECT COUNT(id) FROM files \
+                                                WHERE parent_id=:1;") {
+                let mut rows = stmt.query(&[&folder_id]).unwrap();
+                if let Some(Ok(row)) = rows.next() {
+                    return row.get(0);
+                }
+            }
+        }
+        -1
+    }
+
+    pub fn get_all_keywords(&self) -> Vec<Keyword> {
+        if let Some(ref conn) = self.dbconn {
+            let sql = format!("SELECT {} FROM {}",
+                              Keyword::read_db_columns(),
+                              Keyword::read_db_tables());
+            if let Ok(mut stmt) = conn.prepare(&sql) {
+                let mut keywords: Vec<Keyword> = vec!();
+                let mut rows = stmt.query(&[]).unwrap();
+                while let Some(Ok(row)) = rows.next() {
+                    let keyword = Keyword::read_from(&row);
+                    keywords.push(keyword);
+                }
+                return keywords;
+            }
+        }
+        vec!()
+    }
+
+    pub fn add_fs_file(&self, file: &str) -> LibraryId {
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(c) = conn.execute(
+                "INSERT INTO fsfiles (path) VALUES(:1)",
+                &[&file]) {
+                if c != 1 {
+                    return -1;
+                }
+                return conn.last_insert_rowid();
+            }
+        }
+
+        -1
+    }
+
+    pub fn add_file(&self, folder_id: LibraryId, file: &str, _manage: Managed) -> LibraryId {
+        let mut ret: LibraryId = -1;
+        //DBG_ASSERT(manage == Managed::NO, "manage not supported");
+        //DBG_ASSERT(folder_id != -1, "invalid folder ID");
+        let mime = fwk::MimeType::new(file);
+        let file_type = libfile::mimetype_to_filetype(&mime);
+        let label_id: LibraryId = 0;
+        let orientation: i32;
+        let rating: i32;
+        //let label: String; // XXX fixme
+        let flag: i32;
+        let creation_date: fwk::Time;
+        let xmp: String;
+        let meta = fwk::XmpMeta::new_from_file(file, file_type == libfile::FileType::RAW);
+        if let Some(ref meta) = meta {
+            orientation = meta.orientation().unwrap_or(0);
+            rating = meta.rating().unwrap_or(0);
+            //label = meta.label().unwrap_or(String::from(""));
+            flag = meta.flag().unwrap_or(0);
+            if let Some(ref date) = meta.creation_date() {
+                creation_date = date.time_value();
+            } else {
+                creation_date = 0
+            }
+            xmp = meta.serialize_inline();
+        } else {
+            orientation = 0;
+            rating = 0;
+            //label = String::from("");
+            flag = 0;
+            creation_date = 0;
+            xmp = String::from("");
+        }
+
+        let filename = Self::leaf_name_for_pathname(file).unwrap_or(String::from(""));
+        let fs_file_id = self.add_fs_file(file);
+        if fs_file_id <= 0 {
+            // ERR_OUT("add fsfile failed");
+            return 0;
+        }
+
+        if let Some(ref conn) = self.dbconn {
+            let ifile_type = file_type as i32;
+            let time = fwk::Date::now();
+            ret = match conn.execute("INSERT INTO files (\
+                                main_file, name, parent_id, \
+                                import_date, mod_date, \
+                                orientation, file_date, rating, label, \
+                                file_type, flag, xmp) \
+                                VALUES (\
+                                :1, :2, :3, :4, :5, :6, :7, :8, :9, :10, :11, :12)",
+                               &[&fs_file_id, &filename, &folder_id,
+                                 &time, &time,
+                                 &orientation, &creation_date, &rating, &label_id,
+                                 &ifile_type, &flag, &xmp]) {
+                Ok(c) => {
+                    let mut id = -1;
+                    if c == 1 {
+                        id = conn.last_insert_rowid();
+                        if let Some(mut meta) = meta {
+                            let keywords = meta.keywords();
+                            for k in keywords {
+                                let kwid = self.make_keyword(k);
+                                if kwid != -1 {
+                                    self.assign_keyword(kwid, id);
+                                }
+                            }
+                        }
+                    }
+                    id
+                },
+                Err(_) =>
+                    -1,
+            }
+        }
+
+        ret
+    }
+
+    pub fn make_keyword(&self, keyword: &str) -> LibraryId {
+
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(mut stmt) = conn.prepare("SELECT id FROM keywords WHERE \
+                                                keyword=:1;") {
+                let mut rows = stmt.query(&[&keyword]).unwrap();
+                if let Some(Ok(row)) = rows.next() {
+                    let keyword_id = row.get(0);
+                    if keyword_id > 0 {
+                        return keyword_id;
+                    }
+                }
+            }
+
+            if let Ok(c) = conn.execute(
+                "INSERT INTO keywords (keyword, parent_id) VALUES(:1, 0);",
+                &[&keyword]) {
+                if c != 1 {
+                    return -1;
+                }
+                return conn.last_insert_rowid();
+                // XXX notification
+            }
+
+        }
+        -1
+    }
+
+    pub fn assign_keyword(&self, kw_id: LibraryId, file_id: LibraryId) -> bool {
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(_) = conn.execute("INSERT OR IGNORE INTO keywording\
+                                         (file_id, keyword_id) \
+                                         VALUES(:1, :2)",
+                                        &[&kw_id, &file_id]) {
+                return true;
+            }
+        }
+        false
+    }
+
+    pub fn get_keyword_content(&self, keyword_id: LibraryId) -> Vec<LibFile> {
+        self.get_content(keyword_id, "files.id IN \
+                               (SELECT file_id FROM keywording \
+                               WHERE keyword_id=:1) ")
+    }
+
+    pub fn move_file_to_folder(&self, file_id: LibraryId, folder_id: LibraryId) -> bool  {
+        if let Some(ref conn) = self.dbconn {
+            if let Ok(mut stmt) = conn.prepare("SELECT id FROM folders WHERE \
+                                                id=:1;") {
+                let mut rows = stmt.query(&[&folder_id]).unwrap();
+                if let Some(Ok(_)) = rows.next() {
+                    if let Ok(_) = conn.execute("UPDATE files SET parent_id = :1 \
+                                                     WHERE id = :2;",
+                                                    &[&folder_id, &file_id]) {
+                        return true;
+                    }
+                }
+            }
+        }
+        false
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use std::path::Path;
+    use std::path::PathBuf;
+    use std::fs;
+
+    struct AutoDelete {
+        path: PathBuf
+    }
+    impl AutoDelete {
+        pub fn new(path: &Path) -> AutoDelete {
+            AutoDelete { path: PathBuf::from(path) }
+        }
+    }
+    impl Drop for AutoDelete {
+        fn drop(&mut self) {
+            fs::remove_file(&self.path).is_ok();
+        }
+    }
+
+    #[test]
+    fn library_works() {
+        use super::Library;
+
+        let lib = Library::new(Path::new("."));
+        let _autodelete = AutoDelete::new(lib.dbpath());
+
+        assert!(lib.is_ok());
+        assert!(lib.check_database_version() == super::DB_SCHEMA_VERSION);
+
+        let folder_added = lib.add_folder("foo");
+        assert!(folder_added.is_some());
+        let folder_added = folder_added.unwrap();
+        assert!(folder_added.id() > 0);
+
+        let f = lib.get_folder("foo");
+        assert!(f.is_some());
+        let f = f.unwrap();
+        assert_eq!(folder_added.id(), f.id());
+
+        lib.add_folder("bar");
+        assert!(lib.get_folder("bar").is_some());
+
+        let folders = lib.get_all_folders();
+        assert_eq!(folders.len(), 3);
+
+        let file_id = lib.add_file(folder_added.id(), "foo/myfile", super::Managed::NO);
+        assert!(file_id > 0);
+
+        assert!(!lib.move_file_to_folder(file_id, 100));
+        assert!(lib.move_file_to_folder(file_id, folder_added.id()));
+        let count = lib.count_folder(folder_added.id());
+        assert_eq!(count, 1);
+
+        let fl = lib.get_folder_content(folder_added.id());
+        assert_eq!(fl.len(), count as usize);
+        assert_eq!(fl[0].id(), file_id);
+
+        let kwid1 = lib.make_keyword("foo");
+        assert!(kwid1 > 0);
+        let kwid2 = lib.make_keyword("bar");
+        assert!(kwid2 > 0);
+
+        // duplicate keyword
+        let kwid3 = lib.make_keyword("foo");
+        // should return kwid1 because it already exists.
+        assert_eq!(kwid3, kwid1);
+
+        assert!(lib.assign_keyword(kwid1, file_id));
+        assert!(lib.assign_keyword(kwid2, file_id));
+
+        let fl2 = lib.get_keyword_content(kwid1);
+        assert_eq!(fl2.len(), 1);
+        assert_eq!(fl2[0].id(), file_id);
+
+        let kl = lib.get_all_keywords();
+        assert_eq!(kl.len(), 2);
+    }
+}
diff --git a/src/engine/db/mod.rs b/src/engine/db/mod.rs
index ecf0e78..6cd0204 100644
--- a/src/engine/db/mod.rs
+++ b/src/engine/db/mod.rs
@@ -22,6 +22,17 @@ pub mod fsfile;
 pub mod keyword;
 pub mod libfile;
 pub mod libfolder;
+pub mod library;
 
 pub type LibraryId = i64;
 
+use rusqlite;
+
+pub trait FromDb {
+    /// return the columns for reading from the DB.
+    fn read_db_columns() -> &'static str;
+    /// return the tables for reading from the DB.
+    fn read_db_tables() -> &'static str;
+    /// read a new object from the DB row.
+    fn read_from(row: &rusqlite::Row) -> Self;
+}
diff --git a/src/fwk/base/date.cpp b/src/fwk/base/date.cpp
index 3064521..2cc2d20 100644
--- a/src/fwk/base/date.cpp
+++ b/src/fwk/base/date.cpp
@@ -87,7 +87,7 @@ std::string Date::to_string() const
 {
     char buffer[256];
 
-    snprintf(buffer, 256, "%u:%u:%d %u:%u:%u %c%u%u",
+    snprintf(buffer, 256, "%.4u:%.2u:%.2d %.2u:%.2u:%.2u %c%.2u%.2u",
              m_datetime.year, m_datetime.month,
              m_datetime.day, m_datetime.hour,
              m_datetime.minute, m_datetime.second,
diff --git a/src/fwk/base/date.rs b/src/fwk/base/date.rs
new file mode 100644
index 0000000..5d45555
--- /dev/null
+++ b/src/fwk/base/date.rs
@@ -0,0 +1,148 @@
+/*
+ * niepce - fwk/base/date.rs
+ *
+ * Copyright (C) 2017 Hubert Figuière
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use exempi;
+use libc;
+use std::ptr;
+use std::time::{
+    SystemTime,
+    UNIX_EPOCH
+};
+
+// i64 since rusql doesn't like u64.
+// Like a UNIX time_t (also i64)
+pub type Time = i64;
+
+pub enum Timezone {}
+
+/**
+ * Class to deal with ISO8601 string dates as used by XMP.
+ * Bonus: with a timezone.
+ */
+pub struct Date {
+    datetime: exempi::DateTime,
+    tz: Option<Timezone>,
+}
+
+
+impl Date {
+
+    pub fn new(dt: exempi::DateTime, tz: Option<Timezone>) -> Date {
+        Date { datetime: dt, tz: tz }
+    }
+
+    pub fn new_from_time(t: Time, tz: Option<Timezone>) -> Date {
+        let mut dt = exempi::DateTime::new();
+        make_xmp_date_time(t, &mut dt);
+        Date { datetime: dt, tz: tz }
+    }
+    pub fn xmp_date(&self) -> &exempi::DateTime {
+        &self.datetime
+    }
+
+    pub fn time_value(&self) -> Time {
+        let value = self.xmp_date();
+        let mut dt = libc::tm {
+            tm_sec: value.c.second,
+            tm_min: value.c.minute,
+            tm_hour: value.c.hour,
+            tm_mday: value.c.day,
+            tm_mon: value.c.month,
+            tm_year: value.c.year - 1900,
+            tm_wday: 0, // ignored by mktime()
+            tm_yday: 0, // ignored by mktime()
+            tm_isdst: -1,
+            // this field is supposed to be a glibc extension. oh joy.
+            tm_gmtoff: value.c.tz_sign as i64 * ((value.c.tz_hour as i64 * 3600i64) +
+                                                 (value.c.tz_minute as i64 * 60i64)),
+            tm_zone: ptr::null_mut(),
+        };
+        let date = unsafe { libc::mktime(&mut dt) };
+        //DBG_ASSERT(date != -1, "date is -1");
+
+        date
+    }
+
+    pub fn now() -> Time {
+        let time: i64 = if let Ok(t) = SystemTime::now().duration_since(UNIX_EPOCH) {
+            t.as_secs()
+        } else {
+            0
+        } as i64;
+        time
+    }
+
+}
+
+impl ToString for Date {
+    fn to_string(&self) -> String {
+        format!("{0:04}:{1:02}:{2:02} {3:02}:{4:02}:{5:02} {6}{7:02}{8:02}",
+                self.datetime.c.year, self.datetime.c.month,
+                self.datetime.c.day, self.datetime.c.hour,
+                self.datetime.c.minute, self.datetime.c.second,
+                match self.datetime.c.tz_sign {
+                    exempi::XmpTzSign::West => '-',
+                    _ => '+'
+                },
+                self.datetime.c.tz_hour, self.datetime.c.tz_minute)
+    }
+}
+
+/**
+ * Fill the XmpDateTime %xmp_dt from a %t
+ * @return false if gmtime_r failed.
+ */
+pub fn make_xmp_date_time(t: Time, xmp_dt: &mut exempi::DateTime) -> bool {
+    let pgmt = unsafe { libc::gmtime(&t) };
+    if pgmt.is_null() {
+        return false;
+    }
+    unsafe {
+        xmp_dt.c.year = (*pgmt).tm_year + 1900;
+        xmp_dt.c.month = (*pgmt).tm_mon + 1;
+        xmp_dt.c.day = (*pgmt).tm_mday;
+        xmp_dt.c.hour = (*pgmt).tm_hour;
+        xmp_dt.c.minute = (*pgmt).tm_min;
+        xmp_dt.c.second = (*pgmt).tm_sec;
+    }
+    xmp_dt.c.tz_sign = exempi::XmpTzSign::UTC;
+    xmp_dt.c.tz_hour = 0;
+    xmp_dt.c.tz_minute = 0;
+    xmp_dt.c.nano_second = 0;
+
+    return true;
+}
+
+#[cfg(test)]
+mod test {
+    use super::Date;
+
+    #[test]
+    fn test_date() {
+        let d = Date::new_from_time(0, None);
+        let xmp_dt = d.xmp_date();
+
+        assert_eq!(xmp_dt.c.year, 1970);
+        assert_eq!(xmp_dt.c.month, 1);
+        assert_eq!(xmp_dt.c.day, 1);
+
+        assert_eq!(d.to_string(), "1970:01:01 00:00:00 +0000");
+    }
+
+}
diff --git a/src/fwk/base/mod.rs b/src/fwk/base/mod.rs
index c59bd7b..83354d3 100644
--- a/src/fwk/base/mod.rs
+++ b/src/fwk/base/mod.rs
@@ -18,5 +18,6 @@
  */
 
 pub mod fractions;
+pub mod date;
 
 pub type PropertyIndex = u32;
diff --git a/src/fwk/mod.rs b/src/fwk/mod.rs
index ebd81d5..d3175fc 100644
--- a/src/fwk/mod.rs
+++ b/src/fwk/mod.rs
@@ -32,6 +32,8 @@ pub use self::base::fractions::{
     fraction_to_decimal
 };
 
+pub use self::base::date::*;
+
 pub use self::toolkit::mimetype::{
     MimeType
 };
diff --git a/src/fwk/utils/exempi.rs b/src/fwk/utils/exempi.rs
index 555c91d..2f53046 100644
--- a/src/fwk/utils/exempi.rs
+++ b/src/fwk/utils/exempi.rs
@@ -22,6 +22,7 @@ use std::io::prelude::*;
 use std::path::Path;
 use exempi;
 use exempi::Xmp;
+use fwk::Date;
 
 
 static NIEPCE_XMP_NAMESPACE: &'static str = "http://xmlns.figuiere.net/ns/niepce/1.0";;
@@ -162,8 +163,13 @@ impl XmpMeta {
         return self.xmp.get_property_i32(NIEPCE_XMP_NAMESPACE, "Flag", &mut flags);
     }
 
-    // XXX need fwk::Date()
-    // pub fn creation_date()
+    pub fn creation_date(&self) -> Option<Date> {
+        let mut flags: exempi::PropFlags = exempi::PropFlags::empty();
+        if let Some(date) = self.xmp.get_property_date(NS_EXIF, "DateTimeOriginal", &mut flags) {
+            return Some(Date::new(date, None));
+        }
+        None
+    }
 
     pub fn creation_date_str(&self) -> Option<String> {
         let mut flags: exempi::PropFlags = exempi::PropFlags::empty();
diff --git a/src/lib.rs b/src/lib.rs
index f213538..e1b6839 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -22,6 +22,7 @@ extern crate libc;
 extern crate glib_sys;
 extern crate gio_sys;
 extern crate gio;
+extern crate rusqlite;
 
 pub mod fwk;
 pub mod engine;
diff --git a/src/niepce/Makefile.am b/src/niepce/Makefile.am
index 6efa909..9ae4482 100644
--- a/src/niepce/Makefile.am
+++ b/src/niepce/Makefile.am
@@ -43,6 +43,7 @@ RUST_SOURCES = \
        @top_srcdir@/src/engine/db/filebundle.rs \
        @top_srcdir@/src/engine/db/mod.rs \
        @top_srcdir@/src/engine/db/libfolder.rs \
+       @top_srcdir@/src/engine/db/library.rs \
        @top_srcdir@/src/engine/mod.rs \
        @top_srcdir@/src/fwk/utils/exempi.rs \
        @top_srcdir@/src/fwk/utils/mod.rs \


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]