Commit da36aad5 authored by Luc Didry's avatar Luc Didry
Browse files

Merge branch 'development' into 'master'

Development

Closes #64, #68, #66 et #63

See merge request luc/lutim!30
parents 6921496a d00f151a
Pipeline #42119 passed with stages
in 3 minutes and 20 seconds
zanata.xml merge=ours
......@@ -13,5 +13,5 @@ themes/*
themes/default/templates/data.html.ep
themes/default/templates/raw.html.ep
themes/default/templates/stats.json.ep
themes/default/public/packed/*
tmp/*
.zanata-cache/*
......@@ -15,3 +15,4 @@
* Thor77 (<http://thor77.org>), german translation, among other things
* Quentin Pagès, occitan translation
* Alexis Clairet (<https://github.com/Turboconnard>), paste image to upload ability
* ButterflyOfFire (<https://mastodon.tetaneutral.net/@BoF>), arabic translation
EXTRACTDIR=-D lib -D themes/default/templates
EN=themes/default/lib/Lutim/I18N/en.po
FR=themes/default/lib/Lutim/I18N/fr.po
DE=themes/default/lib/Lutim/I18N/de.po
ES=themes/default/lib/Lutim/I18N/es.po
OC=themes/default/lib/Lutim/I18N/oc.po
XGETTEXT=carton exec local/bin/xgettext.pl
POT=themes/default/lib/Lutim/I18N/lutim.pot
XGETTEXT=carton exec local/bin/xgettext.pl -u
CARTON=carton exec
LUTIM=script/lutim
REAL_LUTIM=script/application
locales:
$(XGETTEXT) $(EXTRACTDIR) -o $(EN) 2>/dev/null
$(XGETTEXT) $(EXTRACTDIR) -o $(FR) 2>/dev/null
$(XGETTEXT) $(EXTRACTDIR) -o $(DE) 2>/dev/null
$(XGETTEXT) $(EXTRACTDIR) -o $(ES) 2>/dev/null
$(XGETTEXT) $(EXTRACTDIR) -o $(OC) 2>/dev/null
$(XGETTEXT) $(EXTRACTDIR) -o $(POT) 2>/dev/null
push-locales: locales
zanata-cli -q -B push
pull-locales:
zanata-cli -q -B pull
stats-locales:
zanata-cli -q stats
podcheck:
podchecker lib/Lutim/DB/Image.pm
......
......@@ -64,6 +64,6 @@ It uses:
* [Moment.js](http://momentjs.com/) for displaying real dates instead of unix timestamps.
* [Fontello](http://fontello.com/) and the [markdown font](https://github.com/dcurtis/markdown-mark/) for the icons, licenses for the fontello icons fonts are in `public/font/LICENSE.txt`
* [Henny Penny](https://www.google.com/fonts/specimen/Henny+Penny) font designed by Olga Umpeleva for [Brownfox](http://brownfox.org)
* [Unite gallery](http://unitegallery.net/) for the gallery
* [PhotoSwipe](http://photoswipe.com/) for the gallery
* [JSZip](https://stuk.github.io/jszip/) for generating a zip containing all the images in the gallery
* [FileSaver.js](https://github.com/eligrey/FileSaver.js/) for saving the zip
......@@ -5,15 +5,13 @@ requires 'Net::SSLeay', '>= 1.81';
requires 'Data::Validate::URI';
requires 'Net::Domain::TLD', '>= 1.73'; # Must have the last version to handle (at least) .xyz and .link
requires 'Mojo::Pg';
requires 'Mojo::SQLite';
requires 'Mojolicious::Plugin::I18N';
requires 'Mojolicious::Plugin::AssetPack', '>= 1.44';
requires 'Mojolicious::Plugin::DebugDumperHelper';
requires 'Mojolicious::Plugin::PgURLHelper';
requires 'Mojolicious::Plugin::StaticCache';
requires "Minion", "== 4.06";
requires 'Minion::Backend::SQLite', "== 0.009";
requires 'CSS::Minifier::XS';
requires 'JavaScript::Minifier::XS';
requires 'ORLite';
requires 'Text::Unidecode';
requires 'DateTime';
requires 'Filesys::DiskUsage';
......
This diff is collapsed.
......@@ -35,6 +35,7 @@ sub startup {
tweet_card_via => '@framasky',
max_file_size => 10*1024*1024,
https => 0,
proposed_delays => '0,1,7,30,365',
default_delay => 0,
max_delay => 0,
token_length => 24,
......@@ -68,16 +69,18 @@ sub startup {
}
push @{$self->renderer->paths}, $self->home->rel_file('themes/default/templates');
push @{$self->static->paths}, $self->home->rel_file('themes/default/public');
# Internationalization
my $lib = $self->home->rel_file('themes/'.$config->{theme}.'/lib');
eval qq(use lib "$lib");
$self->plugin('I18N');
# Compressed assets
$self->plugin('AssetPack' => { pipes => [qw(Combine)] });
# Cache static files
$self->plugin('StaticCache');
# Helpers
$self->plugin('Lutim::Plugin::Helpers');
$self->plugin('Lutim::Plugin::Lang');
# Minion
if ($config->{minion}->{enabled}) {
......@@ -137,20 +140,6 @@ sub startup {
delete @{$wait_for_it}{grep { time - $wait_for_it->{$_} > $c->config->{anti_flood_delay} } keys %{$wait_for_it}} if (defined($wait_for_it));
}
);
$self->hook(after_static => sub {
my $c = shift;
$c->res->headers->cache_control('max-age=2592000, must-revalidate');
});
$self->asset->store->paths($self->static->paths);
$self->asset->process('index.css' => ('css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/animation.css', 'css/uploader.css', 'css/hennypenny.css', 'css/lutim.css', 'css/markdown.css'));
$self->asset->process('stats.css' => ('css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/morris-0.4.3.min.css', 'css/hennypenny.css', 'css/lutim.css'));
$self->asset->process('about.css' => ('css/bootstrap.min.css', 'css/fontello-embedded.css', 'css/hennypenny.css', 'css/lutim.css'));
$self->asset->process('gallery.css' => ('/gallery/css/unite-gallery.css', '/gallery/themes/default/ug-theme-default.css'));
$self->asset->process('index.js' => ('js/bootstrap.min.js', 'js/lutim.js', 'js/dmuploader.min.js'));
$self->asset->process('stats.js' => ('js/bootstrap.min.js', 'js/lutim.js', 'js/raphael-min.js', 'js/morris-0.4.3.min.js', 'js/stats.js'));
$self->asset->process('freeze.js' => ('js/jquery-2.1.0.min.js', 'js/freezeframe.min.js'));
$self->defaults(layout => 'default');
......@@ -181,6 +170,10 @@ sub startup {
to('Controller#stats')->
name('stats');
$r->get('/lang/:l')->
to('Controller#change_lang')->
name('lang');
$r->get('/partial/:file' => sub {
my $c = shift;
$c->render(
......@@ -231,14 +224,24 @@ sub startup {
to('Controller#get_counter')->
name('counter');
$r->get('/about/(:short).(:f)')->
to('Controller#about_img')->
name('about_img');
$r->get('/about/:short/(:key).(:f)')->
to('Controller#about_img')->
name('about_img');
$r->get('/(:short).(:f)')->
to('Controller#short')->
name('short');
$r->get('/:short')->
to('Controller#short');
$r->get('/:short/(:key).(:f)')->
to('Controller#short');
$r->get('/:short/:key')->
to('Controller#short');
}
......
......@@ -41,10 +41,16 @@ use Locale::Maketext::Lexicon {
_style => 'gettext',
'*' => [
Gettext => dirname(__FILE__) . '/I18N/*.po',
Gettext => dirname(__FILE__) . '/../../../default/lib/Lutim/I18N/*.po',
Gettext => $app_dir . 'themes/default/lib/Lutim/I18N/*.po',
]
};
use vars qw($app_dir);
BEGIN {
use Cwd;
my $app_dir = getcwd;
}
1;
EOF
......@@ -58,6 +64,7 @@ FR=lib/Lutim/I18N/fr.po
DE=lib/Lutim/I18N/de.po
ES=lib/Lutim/I18N/es.po
OC=lib/Lutim/I18N/oc.po
AR=lib/Lutim/I18N/ar.po
SEDOPTS=-e "s\@SOME DESCRIPTIVE TITLE\@Lutim language file\@" \\
-e "s\@YEAR THE PACKAGE'S COPYRIGHT HOLDER\@2015 Luc Didry\@" \\
-e "s\@CHARSET\@utf8\@" \\
......@@ -74,6 +81,7 @@ locales:
\$(XGETTEXT) -D templates -D ../default/templates -o \$(DE) 2>/dev/null
\$(XGETTEXT) -D templates -D ../default/templates -o \$(ES) 2>/dev/null
\$(XGETTEXT) -D templates -D ../default/templates -o \$(OC) 2>/dev/null
\$(XGETTEXT) -D templates -D ../default/templates -o \$(AR) 2>/dev/null
sed \$(SEDOPTS) -i \$(EN)
sed \$(SEDOPTS2) -i \$(EN)
sed \$(SEDOPTS) -i \$(FR)
......@@ -83,7 +91,9 @@ locales:
sed \$(SEDOPTS) -i \$(ES)
sed \$(SEDOPTS2) -i \$(ES)
sed \$(SEDOPTS) -i \$(OC)
sed \$(SEDOPTS2) -i \$(OC)
sed \$(SEDOPTS) -i \$(OC)
sed \$(SEDOPTS2) -i \$(AR)
sed \$(SEDOPTS2) -i \$(AR)
EOF
open $f, '>', File::Spec->catfile($home, 'Makefile') or die "Unable to open $home/Makefile: $!";
......
......@@ -45,6 +45,19 @@ sub about {
shift->render(template => 'about');
}
sub change_lang {
my $c = shift;
my $l = $c->param('l');
$c->cookie(lutim_lang => $l, { path => $c->config('prefix') });
if ($c->req->headers->referrer) {
return $c->redirect_to($c->req->headers->referrer);
} else {
return $c->redirect_to('/');
}
}
sub stats {
my $c = shift;
......@@ -71,6 +84,31 @@ sub infos {
);
}
sub about_img {
my $c = shift;
my $short = $c->param('short');
my $image = Lutim::DB::Image->new(app => $c->app, short => $short);
if ($image->enabled && $image->path) {
return $c->render(
json => {
success => true,
data => {
width => $image->width,
height => $image->height,
}
}
);
} else {
return $c->render(
json => {
success => false,
msg => $c->l('Unable to find the image %1.', $short)
}
);
}
}
sub webapp {
my $c = shift;
......@@ -234,7 +272,7 @@ sub delete {
}
},
any => sub {
shift->render_not_found;
$c->helpers->reply->not_found;
}
);
}
......@@ -406,9 +444,9 @@ sub add {
}
}
my $key;
my ($key, $iv);
if ($c->param('crypt') || $c->config('always_encrypt')) {
($upload, $key) = $c->crypt($upload, $filename);
($upload, $key, $iv) = $c->crypt($upload, $filename);
}
$upload->move_to($path);
......@@ -423,6 +461,7 @@ sub add {
->created_by($ip)
->width($width)
->height($height)
->iv($iv)
->write;
# Log image creation
......@@ -514,7 +553,9 @@ sub short {
my $short = $c->param('short');
my $touit = $c->param('t');
my $key = $c->param('key');
my $thumb = $c->param('thumb');
my $thumb;
$thumb = '' if defined $c->param('thumb');
$thumb = $c->param('width') if defined $c->param('width');
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
my $image = Lutim::DB::Image->new(app => $c->app, short => $short);
......@@ -577,12 +618,7 @@ sub short {
);
return $c->redirect_to('/');
} else {
my $expires = ($image->delete_at_day) ? $image->delete_at_day : 360;
my $dt = DateTime->from_epoch( epoch => $expires * 86400 + $image->created_at);
$dt->set_time_zone('GMT');
$expires = $dt->strftime("%a, %d %b %Y %H:%M:%S GMT");
$test = $c->render_file($im_loaded, $image->filename, $image->path, $image->mediatype, $dl, $expires, $image->delete_at_first_view, $key, $thumb);
$test = $c->render_file($im_loaded, $image, $dl, $key, $thumb);
}
}
......@@ -620,7 +656,7 @@ sub short {
return $c->redirect_to('/');
} else {
# Image never existed
$c->render_not_found;
$c->helpers->reply->not_found;
}
}
......
......@@ -17,6 +17,7 @@ has 'last_access_at';
has 'mod_token';
has 'width';
has 'height';
has 'iv';
has 'app';
=head1 NAME
......@@ -64,6 +65,8 @@ Have a look at Lutim::DB::Image::SQLite's code: it's simple and may be more unde
=item B<height> : integer
=item B<iv> : initialization vector for the file encryption
=item B<app> : a mojolicious object
=back
......@@ -125,7 +128,8 @@ sub to_hash {
last_access_at => $c->last_access_at,
mod_token => $c->mod_token,
width => $c->width,
height => $c->height
height => $c->height,
height => $c->iv
};
}
......
......@@ -81,9 +81,9 @@ sub write {
my $c = shift;
if ($c->record) {
$c->app->pg->db->query('UPDATE lutim SET counter = ?, created_at = ?, created_by = ?, delete_at_day = ?, delete_at_first_view = ?, enabled = ?, filename = ?, footprint = ?, height = ?, last_access_at = ?, mediatype = ?, mod_token = ?, path = ?, short = ?, width = ? WHERE short = ?', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width, $c->short);
$c->app->pg->db->query('UPDATE lutim SET counter = ?, created_at = ?, created_by = ?, delete_at_day = ?, delete_at_first_view = ?, enabled = ?, filename = ?, footprint = ?, height = ?, last_access_at = ?, mediatype = ?, mod_token = ?, path = ?, short = ?, width = ?, iv = ? WHERE short = ?', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width, $c->iv, $c->short);
} else {
$c->app->pg->db->query('INSERT INTO lutim (counter, created_at, created_by, delete_at_day, delete_at_first_view, enabled, filename, footprint, height, last_access_at, mediatype, mod_token, path, short, width) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width);
$c->app->pg->db->query('INSERT INTO lutim (counter, created_at, created_by, delete_at_day, delete_at_first_view, enabled, filename, footprint, height, last_access_at, mediatype, mod_token, path, short, width, iv) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width, $c->iv);
$c->record(1);
}
......@@ -220,6 +220,7 @@ sub _slurp {
$c->mod_token($image->{mod_token});
$c->width($image->{width});
$c->height($image->{height});
$c->iv($image->{iv});
$c->record(1);
}
......
# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
package Lutim::DB::Image::SQLite;
use Mojo::Base 'Lutim::DB::Image';
use Lutim::DB::SQLite;
use Mojo::Collection 'c';
has 'record';
has 'record' => 0;
sub new {
my $c = shift;
......@@ -19,13 +18,10 @@ sub accessed {
my $c = shift;
my $time = shift;
$c->record->update(
counter => $c->counter + 1,
last_access_at => $time
);
$c->counter($c->record->counter);
$c->last_access_at($c->record->last_access_at);
$c->app->sqlite->db->query('UPDATE lutim SET counter = counter + 1, last_access_at = ? WHERE short = ?', $time, $c->short);
my $h = $c->app->sqlite->db->query('SELECT counter, last_access_at FROM lutim WHERE short = ?', $c->short)->hashes->first;
$c->counter($h->{counter});
$c->last_access_at($h->{last_access_at});
return $c;
}
......@@ -37,9 +33,9 @@ sub count_delete_at_day_endis {
my $created = shift;
if (defined $created) {
return Lutim::DB::SQLite::Lutim->count('WHERE path IS NOT NULL AND delete_at_day = ? AND enabled = ? AND created_at < ?', $day, $enabled, $created);
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE path IS NOT NULL AND delete_at_day = ? AND enabled = ? AND created_at < ?', $day, $enabled, $created)->hashes->first->{count};
} else {
return Lutim::DB::SQLite::Lutim->count('WHERE path IS NOT NULL AND delete_at_day = ? AND enabled = ?', $day, $enabled);
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE path IS NOT NULL AND delete_at_day = ? AND enabled = ?', $day, $enabled)->hashes->first->{count};
}
}
......@@ -47,7 +43,7 @@ sub count_created_before {
my $c = shift;
my $time = shift;
return Lutim::DB::SQLite::Lutim->count('WHERE path IS NOT NULL AND created_at < ?', $time);
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE path IS NOT NULL AND created_at < ?', $time)->hashes->first->{count};
}
sub select_created_after {
......@@ -56,15 +52,17 @@ sub select_created_after {
my @images;
my @records = Lutim::DB::SQLite::Lutim->select('WHERE path IS NOT NULL AND created_at >= ?', $time);
my $records = $c->app->sqlite->db->query('SELECT * FROM lutim WHERE path IS NOT NULL AND created_at >= ?', $time)->hashes;
for my $e (@records) {
my $i = Lutim::DB::Image->new(app => $c->app);
$i->record($e);
$i->_slurp;
$records->each(
sub {
my ($e, $num) = @_;
my $i = Lutim::DB::Image->new(app => $c->app);
$i->_slurp($e);
push @images, $i;
}
push @images, $i;
}
);
return c(@images);
}
......@@ -72,11 +70,10 @@ sub select_created_after {
sub select_empty {
my $c = shift;
my $record = c(Lutim::DB::SQLite::Lutim->select('WHERE path IS NULL'))->shuffle->first;
$record->update(path => 'used');
my $record = $c->app->sqlite->db->query('SELECT * FROM lutim WHERE path IS NULL')->hashes->shuffle->first;
$c->app->sqlite->db->query('UPDATE lutim SET path = ? WHERE short = ?', 'used', $record->{short});
$c->record($record);
$c = $c->_slurp;
$c = $c->_slurp($record);
return $c;
}
......@@ -85,42 +82,10 @@ sub write {
my $c = shift;
if ($c->record) {
$c->record->update(
counter => $c->counter,
created_at => $c->created_at,
created_by => $c->created_by,
delete_at_day => $c->delete_at_day,
delete_at_first_view => $c->delete_at_first_view,
enabled => $c->enabled,
filename => $c->filename,
footprint => $c->footprint,
height => $c->height,
last_access_at => $c->last_access_at,
mediatype => $c->mediatype,
mod_token => $c->mod_token,
path => $c->path,
short => $c->short,
width => $c->width
);
$c->app->sqlite->db->query('UPDATE lutim SET counter = ?, created_at = ?, created_by = ?, delete_at_day = ?, delete_at_first_view = ?, enabled = ?, filename = ?, footprint = ?, height = ?, last_access_at = ?, mediatype = ?, mod_token = ?, path = ?, short = ?, width = ?, iv = ? WHERE short = ?', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width, $c->iv, $c->short);
} else {
my $record = Lutim::DB::SQLite::Lutim->create(
counter => $c->counter,
created_at => $c->created_at,
created_by => $c->created_by,
delete_at_day => $c->delete_at_day,
delete_at_first_view => $c->delete_at_first_view,
enabled => $c->enabled,
filename => $c->filename,
footprint => $c->footprint,
height => $c->height,
last_access_at => $c->last_access_at,
mediatype => $c->mediatype,
mod_token => $c->mod_token,
path => $c->path,
short => $c->short,
width => $c->width
);
$c->record($record);
$c->app->sqlite->db->query('INSERT INTO lutim (counter, created_at, created_by, delete_at_day, delete_at_first_view, enabled, filename, footprint, height, last_access_at, mediatype, mod_token, path, short, width, iv) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', $c->counter, $c->created_at, $c->created_by, $c->delete_at_day, $c->delete_at_first_view, $c->enabled, $c->filename, $c->footprint, $c->height, $c->last_access_at, $c->mediatype, $c->mod_token, $c->path, $c->short, $c->width, $c->iv);
$c->record(1);
}
return $c;
......@@ -130,30 +95,26 @@ sub count_short {
my $c = shift;
my $short = shift;
return Lutim::DB::SQLite::Lutim->count('WHERE short IS ?', $short);
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE short = ?', $short)->hashes->first->{count};
}
sub count_empty {
my $c = shift;
return Lutim::DB::SQLite::Lutim->count('WHERE path IS NULL');
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE path IS NULL')->hashes->first->{count};
}
sub count_not_empty {
my $c = shift;
return Lutim::DB::SQLite::Lutim->count('WHERE path IS NOT NULL');
return $c->app->sqlite->db->query('SELECT count(short) AS count FROM lutim WHERE path IS NOT NULL')->hashes->first->{count};
}
sub clean_ips_until {
my $c = shift;
my $time = shift;
Lutim::DB::SQLite->do(
'UPDATE lutim SET created_by = "" WHERE path IS NOT NULL AND created_at < ?',
{},
$time
);
$c->app->sqlite->db->query('UPDATE lutim SET created_by = NULL WHERE path IS NOT NULL AND created_at < ?', $time);
return $c;
}
......@@ -164,15 +125,18 @@ sub get_no_longer_viewed_files {
my @images;
my @records = Lutim::DB::SQLite::Lutim->select('WHERE enabled = 1 AND last_access_at < ?', $time);
my $records = $c->app->sqlite->db->query('SELECT * FROM lutim WHERE enabled = 1 AND last_access_at < ?', $time)->{hashes};
for my $e (@records) {
my $i = Lutim::DB::Image->new(app => $c->app);
$i->record($e);
$i->_slurp;
$records->each(
sub {
my ($e, $num) = @_;
my $i = Lutim::DB::Image->new(app => $c->app);
$i->record(1);
$i->_slurp($e);
push @images, $i;
}
push @images, $i;
}
);
return c(@images);
}
......@@ -182,15 +146,17 @@ sub get_images_to_clean {
my @images;
my @records = Lutim::DB::SQLite::Lutim->select('WHERE enabled = 1 AND (delete_at_day * 86400) < (? - created_at) AND delete_at_day != 0', time());
my $records = $c->app->sqlite->db->query('SELECT * FROM lutim WHERE enabled = 1 AND (delete_at_day * 86400) < (? - created_at) AND delete_at_day != 0', time())->hashes;
for my $e (@records) {
my $i = Lutim::DB::Image->new(app => $c->app);
$i->record($e);
$i->_slurp;
$records->each(
sub {
my ($e, $num) = @_;
my $i = Lutim::DB::Image->new(app => $c->app);
$i->_slurp($e);
push @images, $i;
}
push @images, $i;
}
);
return c(@images);
}
......@@ -200,15 +166,17 @@ sub get_50_oldest {
my @images;
my @records = Lutim::DB::SQLite::Lutim->select('WHERE path IS NOT NULL AND enabled = 1 ORDER BY created_at ASC LIMIT 50');
my $records = $c->app->sqlite->db->query('SELECT * FROM lutim WHERE path IS NOT NULL AND enabled = 1 ORDER BY created_at ASC LIMIT 50')->hashes;
for my $e (@records) {
my $i = Lutim::DB::Image->new(app => $c->app);
$i->record($e);
$i->_slurp;
$records->each(
sub {
my ($e, $num) = @_;
my $i = Lutim::DB::Image->new(app => $c->app);
$i->_slurp($e);
push @images, $i;