Compare commits

..

2 Commits

Author SHA1 Message Date
Micha Glave 6ae5fbf334 Db-Relation Tag added. 2020-07-06 17:13:24 +02:00
Micha Glave 3eb6e425f5 Database based caching via diesel & SQLite added. 2020-06-28 16:31:56 +02:00
16 changed files with 468 additions and 11 deletions
+3
View File
@@ -169,3 +169,6 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# SQLite
*.sqlite3
*.db
+17 -7
View File
@@ -1,15 +1,25 @@
[package]
name = "curators"
version = "0.1.0"
authors = ["Micha Glave <mig@xilab.net>"]
authors = ["Micha Glave <coding@migmedia.de>"]
description = "An image gallery, defined by plain files."
edition = "2018"
exclude = [".travis.yml", ".gitignore", "test-data/**"]
description = "Image Gallery. Defined by plain files."
name = "curators"
readme = "README.md"
version = "0.1.0"
[dependencies]
toml = "0.5"
actix-rt = "1.0"
actix-web = "2.0"
diesel = { version ="1.4", features = ["sqlite", "r2d2", "uuid", "chrono"] }
uuid = { version="0.7", features = ["v5"] }
dotenv = "0.15"
env_logger = "0.7"
futures = "0.3"
kamadak-exif = "0.5"
listenfd = "0.3"
structopt = "0.3"
#exempi = "2.5.0"
strum = "0.18.0"
strum_macros = "0.18.0"
kamadak-exif = "0.5"
toml = "0.5"
chrono = "0.4"
anyhow = "1"
+19 -2
View File
@@ -1,10 +1,27 @@
# curators
# curators - Picture data long-term archive
Image Gallery. Defined by plain files.
## Problem definition:
Managing many images from different sources is a challenge. Either there is a quantity limit
by storage space or program (iPhoto), vendor lock or security issues throughout the environment.
## Goal:
Image management for multiple (competing) users. Purely file-based, non-destructive, based on standards (as much as possible). Using
Image management for multiple (competing) users. Purely file-based, non-destructive, based on standards (as much as
possible). Using
* [Exif](https://de.wikipedia.org/wiki/Exchangeable_Image_File_Format)
* [IPTC](https://de.wikipedia.org/wiki/IPTC-IIM-Standard)
* [XMP](https://de.wikipedia.org/wiki/Extensible_Metadata_Platform)
## Principles:
* Original file is not changed.
* Meta-data are next to the image file
* description of contents: people / faces, place, tags
* Authorization
* Reshaping (crop, color correction)
+5
View File
@@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
View File
@@ -0,0 +1 @@
DROP TABLE IF EXISTS images;
@@ -0,0 +1,15 @@
CREATE TABLE images (
id VARCHAR(40) PRIMARY KEY NOT NULL,
path VARCHAR(500) NOT NULL,
title VARCHAR(200),
image_type INTEGER NOT NULL,
last_changed TIMESTAMP NOT NULL,
published BOOLEAN NOT NULL DEFAULT 'f'
);
CREATE INDEX image_path_idx ON images (path);
INSERT INTO images (id, path, title, image_type, last_changed, published) VALUES
('8e153cab-7dc9-46c7-9a72-91e56ac174ca', './BlueSquare.jpg', 'Blaues Quadrat', 1, '2020-05-27 13:33:56.747473', false );
INSERT INTO images (id, path, title, image_type, last_changed, published) VALUES
('ea01f5cd-a532-4232-810c-bae977cc4336', './Canon_40D.jpg', 'Canon 40d', 1, '2020-05-27 13:33:56.747473', false );
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS tags;
+8
View File
@@ -0,0 +1,8 @@
-- Your SQL goes here
CREATE TABLE tags
(
image_id VARCHAR(40) NOT NULL REFERENCES images (id),
tag_type VARCHAR(20) NOT NULL,
name VARCHAR(500) NOT NULL,
PRIMARY KEY (image_id, name)
);
+33
View File
@@ -0,0 +1,33 @@
extern crate curators;
use curators::database::*;
use curators::models::{Image, Tag};
use std::path::Path;
fn main() {
dotenv::dotenv().ok();
let conn = establish_connection();
let results = load_images(&conn, 0, 20);
println!("Displaying {} images", results.len());
for img in results {
println!("{:?} ", img);
delete_image(&conn, img.id).ok();
}
let mut images = Vec::new();
let p = Path::new(r"./BlueSquare.jpg");
images.push(Image::try_from_path(&p).unwrap());
let mut tags = Tag::create_tags(&p);
let p = Path::new(r"./Canon_40D.jpg");
images.push(Image::try_from_path(&p).unwrap());
tags.append(&mut Tag::create_tags(&p));
for i in images {
println!("{:?}", &i);
save_image(&conn, &i).ok();
}
for t in tags {
println!("{:?} = {:?}", &t, save_tags(&conn, &t));
}
}
+6 -1
View File
@@ -5,7 +5,12 @@ use strum_macros::EnumString;
#[derive(EnumString, Debug)]
enum Cmd {
#[strum(serialize = "read", serialize = "r")]
#[strum(
serialize = "read",
serialize = "r",
serialize = "get",
serialize = "g"
)]
Read,
#[strum(
serialize = "write",
+52
View File
@@ -0,0 +1,52 @@
use diesel;
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::env;
use crate::models::{Image, Tag};
use crate::schema::images::dsl::*;
use crate::schema::tags::dsl::*;
type Conn = SqliteConnection;
pub fn establish_connection() -> Conn {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
SqliteConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
pub fn load_images(conn: &Conn, start: i64, limit: i64) -> Vec<Image> {
images
.filter(published.eq(false))
.offset(start)
.limit(limit)
.load::<Image>(conn)
.expect("Error loading images")
}
pub fn load_tags<T>(conn: &Conn, uuid: T) -> Vec<Tag>
where
T: Into<String>,
{
tags.filter(image_id.eq(uuid.into()))
.load::<Tag>(conn)
.expect("Error loading tags.")
}
pub fn save_image(conn: &Conn, img: &Image) -> QueryResult<usize> {
diesel::insert_into(images).values(img).execute(conn)
}
pub fn save_tags(conn: &Conn, tag: &Tag) -> QueryResult<usize> {
diesel::insert_into(tags).values(tag).execute(conn)
}
pub fn delete_image<T>(conn: &Conn, uuid: T) -> QueryResult<usize>
where
T: Into<String>,
{
let uuid_str = uuid.into();
diesel::delete(tags.filter(image_id.eq(&uuid_str))).execute(conn)?;
diesel::delete(images.filter(id.eq(&uuid_str))).execute(conn)
}
+24
View File
@@ -0,0 +1,24 @@
use snafu::{ensure, Backtrace, ErrorCompat, ResultExt, Snafu};
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Could not open config from {}: {}", filename.display(), source))]
OpenConfig {
filename: PathBuf,
source: std::io::Error,
},
#[snafu(display("Could not save config to {}: {}", filename.display(), source))]
SaveConfig {
filename: PathBuf,
source: std::io::Error,
},
#[snafu(display("Could not read file {}: {}", filename.display(), source))]
OpenFile {
filename: PathBuf,
source: std::io::Error,
},
}
+8
View File
@@ -0,0 +1,8 @@
#[macro_use]
extern crate diesel;
extern crate anyhow;
extern crate dotenv;
pub mod database;
pub mod models;
pub mod schema;
+249
View File
@@ -0,0 +1,249 @@
use crate::schema::{images, tags};
use anyhow::{Context, Result};
use chrono::NaiveDateTime;
use diesel::{
deserialize::FromSqlRow,
expression::{helper_types::AsExprOf, AsExpression},
row::Row,
sql_types::{Integer, Text},
sqlite::Sqlite,
Queryable,
};
use std::convert::TryFrom;
use std::error::Error;
use std::{path::Path, time::UNIX_EPOCH};
use strum_macros::{Display, EnumString};
use uuid::Uuid;
#[derive(Debug, Clone, Queryable, Identifiable, Insertable)]
pub struct Image {
pub id: String,
pub path: String,
pub title: Option<String>,
pub image_type: ImageType,
pub last_changed: NaiveDateTime,
pub published: bool,
}
impl Image {
pub fn create_uuid(path: &Path) -> anyhow::Result<Uuid> {
let abs_path = path.canonicalize().context("Path not valid!")?;
let os_path = abs_path.clone().into_os_string();
let path_str = os_path.into_string().expect(&format!(
"Converting path-name problem! {:?}",
path.display()
));
Ok(Uuid::new_v5(&Uuid::NAMESPACE_URL, &path_str.as_bytes()))
}
pub fn try_from_path(path: &Path) -> anyhow::Result<Self> {
let abs_path = path.canonicalize().context("Path not valid!")?;
let os_path = abs_path.clone().into_os_string();
let path_str = os_path.into_string().expect(&format!(
"Converting path-name problem! {:?}",
path.display()
));
let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, &path_str.as_bytes());
let metadata = abs_path
.metadata()
.context(format!("Unable to load Metadata for: {}", path.display()))?;
let time = match metadata.modified() {
Ok(t) => t,
Err(_) => metadata.created().context(format!(
"Unable to read created-timestamp of: {}",
path.display()
))?,
};
let mtime = time.duration_since(UNIX_EPOCH).unwrap();
let image_type = ImageType::from_path(path);
Ok(Image {
id: uuid.to_string(),
path: path_str.to_string(),
title: None,
image_type,
last_changed: NaiveDateTime::from_timestamp(
mtime.as_secs() as i64,
mtime.subsec_nanos() as u32,
),
published: false,
})
}
}
#[derive(Debug, Clone, Queryable, Identifiable, Insertable)]
#[primary_key(image_id, name)]
pub struct Tag {
pub image_id: String,
pub tag_type: TagType,
pub name: String,
}
impl Tag {
pub fn create_tags(rel_path: &Path) -> Vec<Self> {
match Image::create_uuid(rel_path) {
Ok(uuid) => {
let image_id = uuid.to_string();
rel_path
.ancestors()
.filter(|&p| p.display().to_string().len() > 0)
.map(|p: &Path| Self {
image_id: image_id.clone(),
tag_type: TagType::Path,
name: p.display().to_string(),
})
.collect()
}
Err(_) => vec![],
}
}
}
impl TryFrom<&str> for TagType {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"Location" => Ok(TagType::Location),
"Path" => Ok(TagType::Path),
"Face" => Ok(TagType::Face),
x => Err(format!("unknown TagType '{}'", x)),
}
}
}
#[derive(Clone, Copy, Debug, Display, Eq, PartialEq, SqlType, AsExpression, FromSqlRow)]
pub enum ImageType {
UNKNOWN,
JPEG,
PNG,
GIF,
TIFF,
RAW,
HEIF,
}
impl ImageType {
pub fn from_path(path: &Path) -> Self {
let ext = path.extension();
if ext == None {
return ImageType::UNKNOWN;
}
match path.extension().unwrap().to_str() {
Some("heif") => ImageType::HEIF,
Some("hevc") => ImageType::HEIF,
Some("jpeg") => ImageType::JPEG,
Some("jpg") => ImageType::JPEG,
Some("png") => ImageType::PNG,
Some("raw") => ImageType::RAW,
Some("tif") => ImageType::TIFF,
Some("tiff") => ImageType::TIFF,
_ => ImageType::UNKNOWN,
}
}
pub fn to_mimetype(&self) -> &str {
match self {
Self::HEIF => "image/heif",
Self::GIF => "image/gif",
Self::JPEG => "image/jpeg",
Self::PNG => "image/png",
Self::TIFF => "image/tiff",
_ => "application/octet-stream",
}
}
}
impl From<i32> for ImageType {
fn from(int: i32) -> Self {
match int {
1 => ImageType::JPEG,
2 => ImageType::PNG,
3 => ImageType::GIF,
4 => ImageType::TIFF,
5 => ImageType::RAW,
6 => ImageType::HEIF,
_ => ImageType::UNKNOWN,
}
}
}
impl From<&ImageType> for i32 {
fn from(image_type: &ImageType) -> Self {
match image_type {
ImageType::UNKNOWN => 0,
ImageType::JPEG => 1,
ImageType::PNG => 2,
ImageType::GIF => 3,
ImageType::TIFF => 4,
ImageType::RAW => 5,
ImageType::HEIF => 6,
}
}
}
impl From<ImageType> for i32 {
fn from(image_type: ImageType) -> Self {
i32::from(&image_type)
}
}
// traits for diesel
impl diesel::Queryable<Integer, Sqlite> for ImageType {
type Row = i32;
fn build(row: Self::Row) -> Self {
row.into()
}
}
impl FromSqlRow<Integer, Sqlite> for ImageType {
fn build_from_row<R: Row<Sqlite>>(row: &mut R) -> Result<Self, Box<dyn Error + Send + Sync>> {
Ok(i32::build_from_row(row)?.into())
}
}
impl AsExpression<Integer> for ImageType {
type Expression = AsExprOf<i32, Integer>;
fn as_expression(self) -> Self::Expression {
<i32 as AsExpression<Integer>>::as_expression(self as i32)
}
}
impl<'a> AsExpression<Integer> for &'a ImageType {
type Expression = AsExprOf<i32, Integer>;
fn as_expression(self) -> Self::Expression {
<i32 as AsExpression<Integer>>::as_expression(self.into())
}
}
#[derive(
Clone, Copy, Debug, Display, Eq, EnumString, PartialEq, SqlType, AsExpression, FromSqlRow,
)]
#[sqlite_type = "Text"]
pub enum TagType {
Path,
Face,
Location,
}
// traits to help diesel.
impl diesel::Queryable<Text, Sqlite> for TagType {
type Row = String;
fn build(row: Self::Row) -> Self {
TagType::try_from(row.as_str()).unwrap()
}
}
impl AsExpression<Text> for TagType {
type Expression = AsExprOf<String, Text>;
fn as_expression(self) -> Self::Expression {
<String as AsExpression<Text>>::as_expression(self.to_string())
}
}
impl<'a> AsExpression<Text> for &'a TagType {
type Expression = AsExprOf<String, Text>;
fn as_expression(self) -> Self::Expression {
<String as AsExpression<Text>>::as_expression(self.to_string())
}
}
+25
View File
@@ -0,0 +1,25 @@
table! {
images (id) {
id -> Text,
path -> Text,
title -> Nullable<Text>,
image_type -> Integer,
last_changed -> Timestamp,
published -> Bool,
}
}
table! {
tags (image_id, name) {
image_id -> Text,
tag_type -> Text,
name -> Text,
}
}
joinable!(tags -> images (image_id));
allow_tables_to_appear_in_same_query!(
images,
tags,
);