Use structopt.
Code becomes a bit simpler than with raw clap.
This commit is contained in:
parent
f76942bfce
commit
43a259e658
@ -12,7 +12,6 @@ ructe = { version = "^0.6.2", features = ["sass", "mime03"] }
|
||||
[dependencies]
|
||||
brotli2 = "*"
|
||||
chrono = "~0.4.0" # Must match version used by diesel
|
||||
clap = { version = "^2.19", features = [ "color", "wrap_help" ] }
|
||||
diesel = { version = "1.4.0", features = ["r2d2", "chrono", "postgres"] }
|
||||
djangohashers = "*"
|
||||
dotenv = "0.14.0"
|
||||
@ -32,5 +31,6 @@ rust-crypto = "0.2.36"
|
||||
serde = { version = "1.0.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
slug = "0.1"
|
||||
structopt = { version = "0.2.14", features = ["wrap_help"] }
|
||||
time = "*"
|
||||
warp = "0.1.6"
|
||||
|
@ -2,6 +2,7 @@ use super::result::Error;
|
||||
use crate::models::{Camera, Modification, Photo};
|
||||
use crate::myexif::ExifData;
|
||||
use crate::photosdir::PhotosDir;
|
||||
use crate::{DbOpt, DirOpt};
|
||||
use diesel::insert_into;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
@ -9,8 +10,41 @@ use image::GenericImageView;
|
||||
use log::{debug, info, warn};
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
use structopt::StructOpt;
|
||||
|
||||
pub fn crawl(
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
pub struct Findphotos {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
#[structopt(flatten)]
|
||||
photos: DirOpt,
|
||||
|
||||
/// Base directory to search in (relative to the image root).
|
||||
#[structopt()]
|
||||
base: Vec<String>,
|
||||
}
|
||||
|
||||
impl Findphotos {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
let pd = PhotosDir::new(&self.photos.photos_dir);
|
||||
let db = self.db.connect()?;
|
||||
if !self.base.is_empty() {
|
||||
for base in &self.base {
|
||||
crawl(&db, &pd, Path::new(base)).map_err(|e| {
|
||||
Error::Other(format!("Failed to crawl {}: {}", base, e))
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
crawl(&db, &pd, Path::new("")).map_err(|e| {
|
||||
Error::Other(format!("Failed to crawl: {}", e))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn crawl(
|
||||
db: &PgConnection,
|
||||
photos: &PhotosDir,
|
||||
only_in: &Path,
|
||||
@ -25,33 +59,45 @@ pub fn crawl(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn find_sizes(db: &PgConnection, pd: &PhotosDir) -> Result<(), Error> {
|
||||
use crate::schema::photos::dsl as p;
|
||||
let start = Instant::now();
|
||||
let mut c = 0;
|
||||
while start.elapsed().as_secs() < 5 {
|
||||
let photos = Photo::query(true)
|
||||
.filter(p::width.is_null())
|
||||
.filter(p::height.is_null())
|
||||
.order((p::is_public.desc(), p::date.desc().nulls_last()))
|
||||
.limit(10)
|
||||
.load::<Photo>(db)?;
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
pub struct FindSizes {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
#[structopt(flatten)]
|
||||
photos: DirOpt,
|
||||
}
|
||||
|
||||
if photos.is_empty() {
|
||||
break;
|
||||
} else {
|
||||
c += photos.len();
|
||||
}
|
||||
impl FindSizes {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
let db = self.db.connect()?;
|
||||
let pd = PhotosDir::new(&self.photos.photos_dir);
|
||||
use crate::schema::photos::dsl as p;
|
||||
let start = Instant::now();
|
||||
let mut c = 0;
|
||||
while start.elapsed().as_secs() < 5 {
|
||||
let photos = Photo::query(true)
|
||||
.filter(p::width.is_null())
|
||||
.filter(p::height.is_null())
|
||||
.order((p::is_public.desc(), p::date.desc().nulls_last()))
|
||||
.limit(10)
|
||||
.load::<Photo>(&db)?;
|
||||
|
||||
for photo in photos {
|
||||
let path = pd.get_raw_path(&photo);
|
||||
let (width, height) =
|
||||
match ExifData::read_from(&path).and_then(|exif| {
|
||||
Ok((
|
||||
exif.width.ok_or(Error::MissingWidth)?,
|
||||
exif.height.ok_or(Error::MissingHeight)?,
|
||||
))
|
||||
}) {
|
||||
if photos.is_empty() {
|
||||
break;
|
||||
} else {
|
||||
c += photos.len();
|
||||
}
|
||||
|
||||
for photo in photos {
|
||||
let path = pd.get_raw_path(&photo);
|
||||
let (width, height) = match ExifData::read_from(&path)
|
||||
.and_then(|exif| {
|
||||
Ok((
|
||||
exif.width.ok_or(Error::MissingWidth)?,
|
||||
exif.height.ok_or(Error::MissingHeight)?,
|
||||
))
|
||||
}) {
|
||||
Ok((width, height)) => (width, height),
|
||||
Err(e) => {
|
||||
info!(
|
||||
@ -69,21 +115,25 @@ pub fn find_sizes(db: &PgConnection, pd: &PhotosDir) -> Result<(), Error> {
|
||||
(image.width(), image.height())
|
||||
}
|
||||
};
|
||||
diesel::update(p::photos.find(photo.id))
|
||||
.set((p::width.eq(width as i32), p::height.eq(height as i32)))
|
||||
.execute(db)?;
|
||||
debug!("Store img #{} size {} x {}", photo.id, width, height);
|
||||
diesel::update(p::photos.find(photo.id))
|
||||
.set((
|
||||
p::width.eq(width as i32),
|
||||
p::height.eq(height as i32),
|
||||
))
|
||||
.execute(&db)?;
|
||||
debug!("Store img #{} size {} x {}", photo.id, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let e = start.elapsed();
|
||||
info!(
|
||||
"Found size of {} images in {}.{:03} s",
|
||||
c,
|
||||
e.as_secs(),
|
||||
e.subsec_millis(),
|
||||
);
|
||||
Ok(())
|
||||
let e = start.elapsed();
|
||||
info!(
|
||||
"Found size of {} images in {}.{:03} s",
|
||||
c,
|
||||
e.as_secs(),
|
||||
e.subsec_millis(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn save_photo(
|
||||
|
@ -1,10 +1,48 @@
|
||||
use super::result::Error;
|
||||
use crate::models::Photo;
|
||||
use crate::DbOpt;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::result::Error as DieselError;
|
||||
use diesel::update;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{self, BufReader};
|
||||
use structopt::clap::ArgGroup;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
#[structopt(raw(group = "ArgGroup::with_name(\"spec\").required(true)"))]
|
||||
pub struct Makepublic {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
/// Image path to make public
|
||||
#[structopt(group = "spec")]
|
||||
image: Option<String>,
|
||||
/// File listing image paths to make public
|
||||
#[structopt(long, short, group = "spec")]
|
||||
list: Option<String>,
|
||||
}
|
||||
|
||||
impl Makepublic {
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
let db = self.db.connect()?;
|
||||
match (self.list.as_ref().map(AsRef::as_ref), &self.image) {
|
||||
(Some("-"), None) => {
|
||||
let list = io::stdin();
|
||||
by_file_list(&db, list.lock())?;
|
||||
Ok(())
|
||||
}
|
||||
(Some(list), None) => {
|
||||
let list = BufReader::new(File::open(list)?);
|
||||
by_file_list(&db, list)
|
||||
}
|
||||
(None, Some(image)) => one(&db, image),
|
||||
_ => Err(Error::Other("bad command".to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn one(db: &PgConnection, tpath: &str) -> Result<(), Error> {
|
||||
use crate::schema::photos::dsl::*;
|
||||
|
@ -3,64 +3,79 @@ use crate::models::Photo;
|
||||
use crate::photosdir::PhotosDir;
|
||||
use crate::schema::photos::dsl::{date, is_public};
|
||||
use crate::server::SizeTag;
|
||||
use diesel::pg::PgConnection;
|
||||
use crate::{CacheOpt, DbOpt, DirOpt};
|
||||
use diesel::prelude::*;
|
||||
use log::{debug, info};
|
||||
use r2d2_memcache::memcache::Client;
|
||||
use std::time::{Duration, Instant};
|
||||
use structopt::StructOpt;
|
||||
|
||||
/// Make sure all photos are stored in the cache.
|
||||
///
|
||||
/// The work are intentionally handled sequentially, to not
|
||||
/// overwhelm the host while precaching.
|
||||
/// The images are handled in public first, new first order, to have
|
||||
/// the probably most requested images precached as soon as possible.
|
||||
pub fn precache(
|
||||
db: &PgConnection,
|
||||
pd: &PhotosDir,
|
||||
max_secs: u64,
|
||||
) -> Result<(), Error> {
|
||||
let max_time = Duration::from_secs(max_secs);
|
||||
let timer = Instant::now();
|
||||
let mut cache = Client::connect("memcache://127.0.0.1:11211")?;
|
||||
let size = SizeTag::Small;
|
||||
let (mut n, mut n_stored) = (0, 0);
|
||||
let photos = Photo::query(true)
|
||||
.order((is_public.desc(), date.desc().nulls_last()))
|
||||
.load::<Photo>(db)?;
|
||||
let no_expire = 0;
|
||||
for photo in photos {
|
||||
n += 1;
|
||||
let key = &photo.cache_key(size);
|
||||
if cache.get::<Vec<u8>>(key)?.is_none() {
|
||||
let size = size.px();
|
||||
let data = pd.scale_image(&photo, size, size).map_err(|e| {
|
||||
Error::Other(format!(
|
||||
"Failed to scale #{} ({}): {}",
|
||||
photo.id, photo.path, e,
|
||||
))
|
||||
})?;
|
||||
cache.set(key, &data[..], no_expire)?;
|
||||
debug!("Cache: stored {} for {}", key, photo.path);
|
||||
n_stored += 1;
|
||||
if timer.elapsed() > max_time {
|
||||
break;
|
||||
}
|
||||
if n_stored % 64 == 0 {
|
||||
info!(
|
||||
"Checked {} images in cache, added {}, in {:.1?}.",
|
||||
n,
|
||||
n_stored,
|
||||
timer.elapsed()
|
||||
);
|
||||
#[derive(StructOpt)]
|
||||
pub struct Args {
|
||||
#[structopt(flatten)]
|
||||
cache: CacheOpt,
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
#[structopt(flatten)]
|
||||
photos: DirOpt,
|
||||
|
||||
/// Max time (in seconds) to work.
|
||||
#[structopt(default_value = "10")]
|
||||
max_time: u64,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Make sure all photos are stored in the cache.
|
||||
///
|
||||
/// The work are intentionally handled sequentially, to not
|
||||
/// overwhelm the host while precaching.
|
||||
/// The images are handled in public first, new first order, to have
|
||||
/// the probably most requested images precached as soon as possible.
|
||||
pub fn run(&self) -> Result<(), Error> {
|
||||
let max_time = Duration::from_secs(self.max_time);
|
||||
let timer = Instant::now();
|
||||
let mut cache = Client::connect(self.cache.memcached_url.as_ref())?;
|
||||
let size = SizeTag::Small;
|
||||
let (mut n, mut n_stored) = (0, 0);
|
||||
let photos = Photo::query(true)
|
||||
.order((is_public.desc(), date.desc().nulls_last()))
|
||||
.load::<Photo>(&self.db.connect()?)?;
|
||||
let no_expire = 0;
|
||||
let pd = PhotosDir::new(&self.photos.photos_dir);
|
||||
for photo in photos {
|
||||
n += 1;
|
||||
let key = &photo.cache_key(size);
|
||||
if cache.get::<Vec<u8>>(key)?.is_none() {
|
||||
let size = size.px();
|
||||
let data =
|
||||
pd.scale_image(&photo, size, size).map_err(|e| {
|
||||
Error::Other(format!(
|
||||
"Failed to scale #{} ({}): {}",
|
||||
photo.id, photo.path, e,
|
||||
))
|
||||
})?;
|
||||
cache.set(key, &data[..], no_expire)?;
|
||||
debug!("Cache: stored {} for {}", key, photo.path);
|
||||
n_stored += 1;
|
||||
if timer.elapsed() > max_time {
|
||||
break;
|
||||
}
|
||||
if n_stored % 64 == 0 {
|
||||
info!(
|
||||
"Checked {} images in cache, added {}, in {:.1?}.",
|
||||
n,
|
||||
n_stored,
|
||||
timer.elapsed()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
info!(
|
||||
"Checked {} images in cache, added {}, in {:.1?}.",
|
||||
n,
|
||||
n_stored,
|
||||
timer.elapsed()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
info!(
|
||||
"Checked {} images in cache, added {}, in {:.1?}.",
|
||||
n,
|
||||
n_stored,
|
||||
timer.elapsed()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
32
src/env.rs
32
src/env.rs
@ -1,32 +0,0 @@
|
||||
use std::env::var;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
|
||||
pub fn dburl() -> String {
|
||||
require_var("DATABASE_URL", "Database url")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn jwt_key() -> String {
|
||||
require_var("JWT_KEY", "Signing key for jwt")
|
||||
}
|
||||
|
||||
pub fn require_var(name: &str, desc: &str) -> String {
|
||||
match var(name) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
println!("{} needed in {}: {}", desc, name, error);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn photos_dir() -> PathBuf {
|
||||
PathBuf::from(&*env_or("RPHOTOS_DIR", "/home/kaj/Bilder/foto"))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn env_or(name: &str, default: &str) -> String {
|
||||
var(name).unwrap_or_else(|_err| default.to_string())
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
use crate::models::{Coord, Place};
|
||||
use crate::DbOpt;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use diesel::result::{DatabaseErrorKind, Error as DieselError};
|
||||
@ -6,6 +7,50 @@ use log::{debug, info, warn};
|
||||
use reqwest::{self, Client, Response};
|
||||
use serde_json::Value;
|
||||
use slug::slugify;
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
pub struct Fetchplaces {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
/// Max number of photos to use for --auto
|
||||
#[structopt(long, short, default_value = "5")]
|
||||
limit: i64,
|
||||
/// Fetch data for photos with position but lacking places.
|
||||
#[structopt(long, short)]
|
||||
auto: bool,
|
||||
/// Image ids to fetch place data for
|
||||
photos: Vec<i32>,
|
||||
}
|
||||
|
||||
impl Fetchplaces {
|
||||
pub fn run(&self) -> Result<(), super::adm::result::Error> {
|
||||
let db = self.db.connect()?;
|
||||
if self.auto {
|
||||
println!("Should find {} photos to fetch places for", self.limit);
|
||||
use crate::schema::photo_places::dsl as place;
|
||||
use crate::schema::positions::dsl as pos;
|
||||
let result = pos::positions
|
||||
.select((pos::photo_id, (pos::latitude, pos::longitude)))
|
||||
.filter(pos::photo_id.ne_all(
|
||||
place::photo_places.select(place::photo_id).distinct(),
|
||||
))
|
||||
.order(pos::photo_id.desc())
|
||||
.limit(self.limit)
|
||||
.load::<(i32, Coord)>(&db)?;
|
||||
for (photo_id, coord) in result {
|
||||
println!("Find places for #{}, {:?}", photo_id, coord);
|
||||
update_image_places(&db, photo_id)?;
|
||||
}
|
||||
} else {
|
||||
for photo in &self.photos {
|
||||
update_image_places(&db, *photo)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_image_places(c: &PgConnection, image: i32) -> Result<(), Error> {
|
||||
use crate::schema::positions::dsl::*;
|
||||
|
291
src/main.rs
291
src/main.rs
@ -4,7 +4,6 @@
|
||||
extern crate diesel;
|
||||
|
||||
mod adm;
|
||||
mod env;
|
||||
mod fetch_places;
|
||||
mod models;
|
||||
mod myexif;
|
||||
@ -16,121 +15,95 @@ mod server;
|
||||
use crate::adm::result::Error;
|
||||
use crate::adm::stats::show_stats;
|
||||
use crate::adm::{findphotos, makepublic, precache, storestatics, users};
|
||||
use crate::env::{dburl, photos_dir};
|
||||
use crate::models::Coord;
|
||||
use crate::photosdir::PhotosDir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use dotenv::dotenv;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader};
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use structopt::StructOpt;
|
||||
|
||||
/// Command line interface for rphotos.
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
enum RPhotos {
|
||||
/// Make specific image(s) public.
|
||||
///
|
||||
/// The image path(s) are relative to the image root.
|
||||
Makepublic(makepublic::Makepublic),
|
||||
/// Get place tags for photos by looking up coordinates in OSM
|
||||
Fetchplaces(fetch_places::Fetchplaces),
|
||||
/// Find new photos in the photo directory
|
||||
Findphotos(findphotos::Findphotos),
|
||||
/// Find sizes of images lacking that info in db
|
||||
FindSizes(findphotos::FindSizes),
|
||||
/// Make sure the photos has thumbnails stored in cache.
|
||||
///
|
||||
/// The time limit is checked after each stored image, so the
|
||||
/// command will complete in slightly more than the max time and
|
||||
/// one image will be processed even if the max time is zero.
|
||||
Precache(precache::Args),
|
||||
/// Show some statistics from the database
|
||||
Stats(DbOpt),
|
||||
/// Store statics as files for a web server
|
||||
Storestatics {
|
||||
/// Directory to store the files in
|
||||
dir: String,
|
||||
},
|
||||
/// List existing users
|
||||
Userlist {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
},
|
||||
/// Set password for a (new or existing) user
|
||||
Userpass {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
/// Username to set password for
|
||||
// TODO: Use a special type that only accepts nice user names.
|
||||
user: String,
|
||||
},
|
||||
/// Run the rphotos web server.
|
||||
Runserver(server::Args),
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
struct CacheOpt {
|
||||
/// How to connect to memcached.
|
||||
#[structopt(
|
||||
long,
|
||||
env = "MEMCACHED_SERVER",
|
||||
default_value = "memcache://127.0.0.1:11211"
|
||||
)]
|
||||
memcached_url: String,
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
struct DbOpt {
|
||||
/// How to connect to the postgres database.
|
||||
#[structopt(long, env = "DATABASE_URL", hide_env_values = true)]
|
||||
db_url: String,
|
||||
}
|
||||
|
||||
impl DbOpt {
|
||||
fn connect(&self) -> Result<PgConnection, ConnectionError> {
|
||||
PgConnection::establish(&self.db_url)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
struct DirOpt {
|
||||
/// Path to the root directory storing all actual photos.
|
||||
#[structopt(long, env = "RPHOTOS_DIR")]
|
||||
photos_dir: PathBuf,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dotenv().ok();
|
||||
env_logger::init();
|
||||
let args = App::new("rphotos")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Command line interface for rphotos")
|
||||
.subcommand(
|
||||
SubCommand::with_name("findphotos")
|
||||
.about("Find new photos in the photo directory")
|
||||
.arg(Arg::with_name("BASE").multiple(true).help(
|
||||
"Base directory to search in (relative to the \
|
||||
image root).",
|
||||
)),
|
||||
).subcommand(
|
||||
SubCommand::with_name("find-sizes")
|
||||
.about("Find sizes of images lacking that info in db"),
|
||||
).subcommand(
|
||||
SubCommand::with_name("stats")
|
||||
.about("Show some statistics from the database"),
|
||||
).subcommand(
|
||||
SubCommand::with_name("userlist").about("List existing users"),
|
||||
).subcommand(
|
||||
SubCommand::with_name("userpass")
|
||||
.about("Set password for a (new or existing) user")
|
||||
.arg(
|
||||
Arg::with_name("USER")
|
||||
.required(true)
|
||||
.help("Username to set password for"),
|
||||
),
|
||||
).subcommand(
|
||||
SubCommand::with_name("fetchplaces")
|
||||
.about("Get place tags for photos by looking up coordinates in OSM")
|
||||
.arg(
|
||||
Arg::with_name("LIMIT")
|
||||
.long("limit")
|
||||
.short("l")
|
||||
.takes_value(true)
|
||||
.default_value("5")
|
||||
.help("Max number of photos to use for --auto")
|
||||
).arg(
|
||||
Arg::with_name("AUTO")
|
||||
.long("auto")
|
||||
.help("Fetch data for photos with position but \
|
||||
lacking places.")
|
||||
).arg(
|
||||
Arg::with_name("PHOTOS")
|
||||
.required_unless("AUTO").multiple(true)
|
||||
.help("Image ids to fetch place data for"),
|
||||
),
|
||||
).subcommand(
|
||||
SubCommand::with_name("makepublic")
|
||||
.about("make specific image(s) public")
|
||||
.arg(
|
||||
Arg::with_name("LIST")
|
||||
.long("list")
|
||||
.short("l")
|
||||
.takes_value(true)
|
||||
.help("File listing image paths to make public"),
|
||||
).arg(
|
||||
Arg::with_name("IMAGE")
|
||||
.required_unless("LIST")
|
||||
.help("Image path to make public"),
|
||||
).after_help(
|
||||
"The image path(s) are relative to the image root.",
|
||||
),
|
||||
).subcommand(
|
||||
SubCommand::with_name("precache")
|
||||
.about("Make sure the photos has thumbnails stored in cache.")
|
||||
.arg(
|
||||
Arg::with_name("MAXTIME")
|
||||
.long("max-time")
|
||||
.default_value("10")
|
||||
.help("Max time (in seconds) to work")
|
||||
).after_help(
|
||||
"The time limit is checked after each stored image, \
|
||||
so the command will complete in slightly more than \
|
||||
the max time and one image will be processed even \
|
||||
if the max time is zero."
|
||||
)
|
||||
).subcommand(
|
||||
SubCommand::with_name("storestatics")
|
||||
.about("Store statics as files for a web server")
|
||||
.arg(
|
||||
Arg::with_name("DIR")
|
||||
.required(true)
|
||||
.help("Directory to store the files in"),
|
||||
),
|
||||
).subcommand(
|
||||
SubCommand::with_name("runserver")
|
||||
.arg(
|
||||
Arg::with_name("PIDFILE")
|
||||
.long("pidfile")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
"Write (and read, if --replace) a pid file with \
|
||||
the name given as <PIDFILE>.",
|
||||
),
|
||||
).arg(Arg::with_name("REPLACE").long("replace").help(
|
||||
"Kill old server (identified by pid file) before running",
|
||||
)),
|
||||
).get_matches();
|
||||
|
||||
match run(&args) {
|
||||
match run(&RPhotos::from_args()) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
@ -139,97 +112,19 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: &ArgMatches) -> Result<(), Error> {
|
||||
match args.subcommand() {
|
||||
("findphotos", Some(args)) => {
|
||||
let pd = PhotosDir::new(photos_dir());
|
||||
let db = get_db()?;
|
||||
if let Some(bases) = args.values_of("BASE") {
|
||||
for base in bases {
|
||||
findphotos::crawl(&db, &pd, Path::new(&base)).map_err(
|
||||
|e| {
|
||||
Error::Other(format!(
|
||||
"Failed to crawl {}: {}",
|
||||
base, e,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
findphotos::crawl(&db, &pd, Path::new("")).map_err(|e| {
|
||||
Error::Other(format!("Failed to crawl: {}", e))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
("find-sizes", Some(_args)) => {
|
||||
findphotos::find_sizes(&get_db()?, &PhotosDir::new(photos_dir()))
|
||||
}
|
||||
("makepublic", Some(args)) => {
|
||||
let db = get_db()?;
|
||||
match args.value_of("LIST") {
|
||||
Some("-") => {
|
||||
let list = io::stdin();
|
||||
makepublic::by_file_list(&db, list.lock())?;
|
||||
Ok(())
|
||||
}
|
||||
Some(f) => {
|
||||
let list = File::open(f)?;
|
||||
let list = BufReader::new(list);
|
||||
makepublic::by_file_list(&db, list)
|
||||
}
|
||||
None => makepublic::one(&db, args.value_of("IMAGE").unwrap()),
|
||||
}
|
||||
}
|
||||
("stats", Some(_args)) => show_stats(&get_db()?),
|
||||
("userlist", Some(_args)) => users::list(&get_db()?),
|
||||
("fetchplaces", Some(args)) => {
|
||||
let db = get_db()?;
|
||||
if args.is_present("AUTO") {
|
||||
let limit = args.value_of("LIMIT").unwrap().parse()?;
|
||||
println!("Should find {} photos to fetch places for", limit);
|
||||
use crate::schema::photo_places::dsl as place;
|
||||
use crate::schema::positions::dsl as pos;
|
||||
let result = pos::positions
|
||||
.select((pos::photo_id, (pos::latitude, pos::longitude)))
|
||||
.filter(pos::photo_id.ne_all(
|
||||
place::photo_places.select(place::photo_id).distinct(),
|
||||
))
|
||||
.order(pos::photo_id.desc())
|
||||
.limit(limit)
|
||||
.load::<(i32, Coord)>(&db)?;
|
||||
for (photo_id, coord) in result {
|
||||
println!("Find places for #{}, {:?}", photo_id, coord);
|
||||
fetch_places::update_image_places(&db, photo_id)?;
|
||||
}
|
||||
} else {
|
||||
for photo in args.values_of("PHOTOS").unwrap() {
|
||||
fetch_places::update_image_places(&db, photo.parse()?)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
("userpass", Some(args)) => {
|
||||
users::passwd(&get_db()?, args.value_of("USER").unwrap())
|
||||
}
|
||||
("precache", Some(args)) => precache::precache(
|
||||
&get_db()?,
|
||||
&PhotosDir::new(photos_dir()),
|
||||
args.value_of("MAXTIME").unwrap().parse()?,
|
||||
),
|
||||
("storestatics", Some(args)) => {
|
||||
storestatics::to_dir(args.value_of("DIR").unwrap())
|
||||
}
|
||||
("runserver", Some(args)) => server::run(args),
|
||||
_ => {
|
||||
println!("No subcommand given.\n\n{}", args.usage());
|
||||
Ok(())
|
||||
}
|
||||
fn run(args: &RPhotos) -> Result<(), Error> {
|
||||
match args {
|
||||
RPhotos::Findphotos(cmd) => cmd.run(),
|
||||
RPhotos::FindSizes(cmd) => cmd.run(),
|
||||
RPhotos::Makepublic(cmd) => cmd.run(),
|
||||
RPhotos::Stats(db) => show_stats(&db.connect()?),
|
||||
RPhotos::Userlist { db } => users::list(&db.connect()?),
|
||||
RPhotos::Userpass { db, user } => users::passwd(&db.connect()?, user),
|
||||
RPhotos::Fetchplaces(cmd) => cmd.run(),
|
||||
RPhotos::Precache(cmd) => cmd.run(),
|
||||
RPhotos::Storestatics { dir } => storestatics::to_dir(dir),
|
||||
RPhotos::Runserver(ra) => server::run(ra),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_db() -> Result<PgConnection, ConnectionError> {
|
||||
PgConnection::establish(&dburl())
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||
|
@ -11,8 +11,10 @@ pub struct PhotosDir {
|
||||
}
|
||||
|
||||
impl PhotosDir {
|
||||
pub fn new(basedir: PathBuf) -> Self {
|
||||
PhotosDir { basedir }
|
||||
pub fn new(basedir: &Path) -> Self {
|
||||
PhotosDir {
|
||||
basedir: basedir.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::env::photos_dir;
|
||||
use crate::photosdir::PhotosDir;
|
||||
use crypto::sha2::Sha256;
|
||||
use diesel::pg::PgConnection;
|
||||
@ -8,6 +7,7 @@ use log::{debug, error, warn};
|
||||
use r2d2_memcache::r2d2::Error;
|
||||
use r2d2_memcache::MemcacheConnectionManager;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use warp::filters::{cookie, BoxedFilter};
|
||||
@ -23,10 +23,15 @@ type PooledMemcache = PooledConnection<MemcacheConnectionManager>;
|
||||
pub fn create_session_filter(
|
||||
db_url: &str,
|
||||
memcache_server: &str,
|
||||
jwt_secret: String,
|
||||
photos_dir: &Path,
|
||||
jwt_secret: &str,
|
||||
) -> BoxedFilter<(Context,)> {
|
||||
let global =
|
||||
Arc::new(GlobalContext::new(db_url, memcache_server, jwt_secret));
|
||||
let global = Arc::new(GlobalContext::new(
|
||||
db_url,
|
||||
memcache_server,
|
||||
photos_dir,
|
||||
jwt_secret,
|
||||
));
|
||||
warp::any()
|
||||
.and(path::full())
|
||||
.and(cookie::optional("EXAUTH"))
|
||||
@ -53,7 +58,12 @@ struct GlobalContext {
|
||||
}
|
||||
|
||||
impl GlobalContext {
|
||||
fn new(db_url: &str, memcache_server: &str, jwt_secret: String) -> Self {
|
||||
fn new(
|
||||
db_url: &str,
|
||||
memcache_server: &str,
|
||||
photos_dir: &Path,
|
||||
jwt_secret: &str,
|
||||
) -> Self {
|
||||
let db_manager = ConnectionManager::<PgConnection>::new(db_url);
|
||||
let mc_manager = MemcacheConnectionManager::new(memcache_server);
|
||||
GlobalContext {
|
||||
@ -61,12 +71,12 @@ impl GlobalContext {
|
||||
.connection_timeout(Duration::from_secs(1))
|
||||
.build(db_manager)
|
||||
.expect("Posgresql pool"),
|
||||
photosdir: PhotosDir::new(photos_dir()),
|
||||
photosdir: PhotosDir::new(photos_dir),
|
||||
memcache_pool: Pool::builder()
|
||||
.connection_timeout(Duration::from_secs(1))
|
||||
.build(mc_manager)
|
||||
.expect("Memcache pool"),
|
||||
jwt_secret,
|
||||
jwt_secret: jwt_secret.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,13 +10,12 @@ pub use self::context::Context;
|
||||
use self::render_ructe::RenderRucte;
|
||||
use self::splitlist::*;
|
||||
use self::views_by_date::*;
|
||||
use super::{CacheOpt, DbOpt, DirOpt};
|
||||
use crate::adm::result::Error;
|
||||
use crate::env::{dburl, env_or, jwt_key};
|
||||
use crate::models::{Person, Photo, Place, Tag};
|
||||
use crate::pidfiles::handle_pid_file;
|
||||
use crate::templates::{self, Html};
|
||||
use chrono::Datelike;
|
||||
use clap::ArgMatches;
|
||||
use diesel::prelude::*;
|
||||
use djangohashers;
|
||||
use image;
|
||||
@ -24,10 +23,40 @@ use log::info;
|
||||
use mime;
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
use structopt::StructOpt;
|
||||
use warp::filters::path::Tail;
|
||||
use warp::http::{header, Response, StatusCode};
|
||||
use warp::{self, reply, Filter, Rejection, Reply};
|
||||
|
||||
#[derive(StructOpt)]
|
||||
#[structopt(rename_all = "kebab-case")]
|
||||
pub struct Args {
|
||||
#[structopt(flatten)]
|
||||
db: DbOpt,
|
||||
#[structopt(flatten)]
|
||||
cache: CacheOpt,
|
||||
#[structopt(flatten)]
|
||||
photos: DirOpt,
|
||||
|
||||
/// Write (and read, if --replace) a pid file with the name
|
||||
/// given as <PIDFILE>.
|
||||
#[structopt(long)]
|
||||
pidfile: Option<String>,
|
||||
/// Kill old server (identified by pid file) before running.
|
||||
#[structopt(long, short)]
|
||||
replace: bool,
|
||||
/// Socket addess for rphotos to listen on.
|
||||
#[structopt(
|
||||
long,
|
||||
env = "RPHOTOS_LISTEN",
|
||||
default_value = "127.0.0.1:6767"
|
||||
)]
|
||||
listen: SocketAddr,
|
||||
/// Signing key for jwt
|
||||
#[structopt(long, env = "JWT_KEY", hide_env_values = true)]
|
||||
jwt_key: String,
|
||||
}
|
||||
|
||||
pub struct PhotoLink {
|
||||
pub title: Option<String>,
|
||||
pub href: String,
|
||||
@ -147,14 +176,15 @@ impl PhotoLink {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &ArgMatches) -> Result<(), Error> {
|
||||
if let Some(pidfile) = args.value_of("PIDFILE") {
|
||||
handle_pid_file(pidfile, args.is_present("REPLACE")).unwrap()
|
||||
pub fn run(args: &Args) -> Result<(), Error> {
|
||||
if let Some(pidfile) = &args.pidfile {
|
||||
handle_pid_file(&pidfile, args.replace).unwrap()
|
||||
}
|
||||
let session_filter = create_session_filter(
|
||||
&dburl(),
|
||||
&env_or("MEMCACHED_SERVER", "memcache://127.0.0.1:11211"),
|
||||
jwt_key(),
|
||||
&args.db.db_url,
|
||||
&args.cache.memcached_url,
|
||||
&args.photos.photos_dir,
|
||||
&args.jwt_key,
|
||||
);
|
||||
let s = move || session_filter.clone();
|
||||
use warp::filters::query::query;
|
||||
@ -190,10 +220,7 @@ pub fn run(args: &ArgMatches) -> Result<(), Error> {
|
||||
.or(get().and(path("ac")).and(path("tag")).and(s()).and(query()).map(auto_complete_tag))
|
||||
.or(get().and(path("ac")).and(path("person")).and(s()).and(query()).map(auto_complete_person))
|
||||
.or(path("adm").and(admin::routes(s())));
|
||||
let addr = env_or("RPHOTOS_LISTEN", "127.0.0.1:6767")
|
||||
.parse::<SocketAddr>()
|
||||
.map_err(|e| Error::Other(format!("{}", e)))?;
|
||||
warp::serve(routes.recover(customize_error)).run(addr);
|
||||
warp::serve(routes.recover(customize_error)).run(args.listen);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user