[niepce: 14/29] engine+rust: implement Library in Rust.
- From: Hubert Figuière <hub src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [niepce: 14/29] engine+rust: implement Library in Rust.
- Date: Fri, 22 Sep 2017 00:42:54 +0000 (UTC)
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]