67 Commits
0.1 ... 0.2

Author SHA1 Message Date
Luc Didry
123d597c5f Add Crypt::Blowfish dependancy in cpanfile 2014-03-07 03:33:00 +01:00
Luc Didry
096bf1acc3 Update Changelog 2014-03-07 03:23:16 +01:00
Luc Didry
5349d327ed Fix #20
Thumbnails in response
2014-03-07 03:17:57 +01:00
Luc Didry
ad0e799601 Better maximum delay handling (related to issue #8) 2014-03-07 00:16:15 +01:00
Luc Didry
ff8ab141f9 Update png logo from blurried xcf logo 2014-03-06 23:48:39 +01:00
Luc Didry
ebb9cafb1e Small UI changes 2014-03-06 23:46:08 +01:00
Luc Didry
d6cbaa1a6f Better HTML compatibility 2014-03-06 23:45:29 +01:00
Luc Didry
2fae7345d9 Fix #22
Files encryption
2014-03-06 23:43:59 +01:00
Luc Didry
1f93835060 Bugfix for noscript version 2014-03-06 14:59:06 +01:00
Luc Didry
2786a3bda2 Add remote port in IP log 2014-03-06 14:31:00 +01:00
Luc Didry
9a283274e0 Small UI changes 2014-03-06 14:29:31 +01:00
Luc Didry
8953ddf840 Improve CORS 2014-02-28 23:38:34 +01:00
Luc Didry
f40c8e1195 Add improved xcf logo 2014-02-28 23:28:48 +01:00
Luc Didry
e794edd6d1 Put methods in separate controller 2014-02-28 23:14:55 +01:00
Luc Didry
b8401e259e Add Changelog 2014-02-28 22:11:04 +01:00
Luc Didry
afb5eb3ecd Add contributors 2014-02-28 22:09:51 +01:00
Luc Didry
551b9cadc5 Add HTTP headers Expires and Content-Cache 2014-02-28 21:58:49 +01:00
Luc Didry
0b106f6b99 Merge branch 'jcb-master' into dev
Conflicts:
	README.md
2014-02-24 23:20:32 +01:00
Jean-Christophe Bach
04b220a665 * remove unbreakable spaces 2014-02-24 22:27:33 +01:00
Jean-Christophe Bach
b1c73555aa * typos 2014-02-24 22:27:11 +01:00
Luc Didry
37feb11430 Fix #8 2014-02-24 21:57:50 +01:00
Luc Didry
9c896f1a59 Fix #18 2014-02-24 21:00:38 +01:00
Luc Didry
e014bc229b Cron task to delete expired images 2014-02-24 20:12:10 +01:00
Luc Didry
9787a7210a Fix #7 2014-02-24 14:47:10 +01:00
Luc Didry
662ef1c461 Small changes in README 2014-02-23 22:18:08 +01:00
Luc Didry
54d8bf12a8 Fix #19 2014-02-23 21:16:46 +01:00
Luc Didry
fb51b92e09 Fix #17 2014-02-23 13:04:16 +01:00
Luc Didry
96dc4758ef Fix HTML validity in data stats table 2014-02-20 13:54:23 +01:00
Luc Didry
584e9d2dd2 Update lutim.conf.template
Comment contact option, in order to force editing lutim.conf before
first run
2014-02-20 13:41:49 +01:00
Luc Didry
dee5d0a22e Fix #13
The mime type detection module was adding "x-" to some mime types.
2014-02-20 11:44:43 +01:00
Luc Didry
682923726c Put provisioning call in after_dispatch hook 2014-02-20 11:42:54 +01:00
Luc Didry
f8807288c6 Small bugfix
The informations link was not displayed on stats and about pages
2014-02-20 11:41:55 +01:00
Luc Didry
fc2b866238 Add Homepage button on stats page 2014-02-20 11:41:18 +01:00
Luc Didry
373ec23bbc Add possibility to display a message on all LUTIm pages 2014-02-20 11:40:31 +01:00
Luc Didry
b0b905b139 Fix #9
Allow to take action when a settable maximum size is reached
Three actions implemented:
* warn
* stop-upload
* delete
2014-02-20 11:31:08 +01:00
Luc Didry
49f1838ed0 Update README 2014-02-20 01:15:43 +01:00
Luc Didry
2c9eca80e3 Fix #11
+ update README for config options + cron jobs
2014-02-20 01:14:26 +01:00
Luc Didry
96072d0a2a Small typo in stats.pm 2014-02-20 00:37:21 +01:00
Luc Didry
b2a408a207 Fix #6 2014-02-20 00:27:23 +01:00
Luc Didry
8eb2c200c8 Put informations on a separate page for those with no script 2014-02-19 23:50:41 +01:00
Luc Didry
be80462d2e Remove --deployement for carton install in README 2014-02-18 09:51:52 +01:00
Luc Didry
9d9e29e906 Better x-forwarded-for detection 2014-02-18 02:06:11 +01:00
Luc Didry
fcd1b1489b Fix #10
Add configuration doc
Change provisionning option to provisioning (with back-compatibility)
2014-02-17 21:47:50 +01:00
Luc Didry
cacd1a46ee Better filetype detection 2014-02-17 21:08:05 +01:00
Luc Didry
d3bde59421 Update informations (for previous logs anonymising commit) 2014-02-17 20:40:54 +01:00
Luc Didry
de1dff9f35 Forgot & in localization files for HTML validity 2014-02-17 20:35:26 +01:00
Luc Didry
096e2b3e07 Anonymise logs 2014-02-17 20:27:13 +01:00
Luc Didry
bc7a7fabf0 Better HTML validity 2014-02-17 19:58:33 +01:00
Luc Didry
6561a60bdc Add logging of hits on index page 2014-02-17 19:34:03 +01:00
Luc Didry
5373ec63a8 Set max height and max width for images for twitter 2014-02-17 18:55:36 +01:00
Luc Didry
23a74b2b86 Set https to 0 in conf template 2014-02-16 20:42:02 +01:00
Luc Didry
51e0483652 Bugfix https (again) 2014-02-16 20:39:42 +01:00
Luc Didry
6766ed6aef Bugfix for https connections 2014-02-16 19:17:02 +01:00
Luc Didry
dbd68796e8 Make links https if access by https 2014-02-16 18:57:01 +01:00
Luc Didry
3388577790 Add favicon 2014-02-16 18:11:11 +01:00
Luc Didry
5da1e9ee03 Fix #1
More options for suppression delay
2014-02-16 17:34:29 +01:00
Luc Didry
992850d99b Fix #5 2014-02-16 16:55:05 +01:00
Luc Didry
1f7559effc Fix #4
Add upload progress bar
2014-02-16 16:13:38 +01:00
Luc Didry
4cb9f4adf5 Change Godo main page link in README 2014-02-16 01:26:29 +01:00
Luc Didry
239f49b2e4 Add Godo main page link in README 2014-02-16 01:25:30 +01:00
Luc Didry
6f9c1bcdf4 Fix logo img link in README again 2014-02-16 01:21:01 +01:00
Luc Didry
3a11b93d35 Fix logo img link in README 2014-02-16 01:18:19 +01:00
Luc Didry
4b11f1b38c Add LUTIm's logo 2014-02-16 01:13:56 +01:00
Luc Didry
b3420967ee Merge pull request #2 from goofy-bz/patch-2
Update en.pm
2014-02-16 01:01:40 +01:00
Luc Didry
067b2198fd Merge pull request #3 from goofy-bz/patch-1
Update fr.pm
2014-02-16 01:01:15 +01:00
goofy-bz
b1ecf6aa4b Update en.pm
minor typo fix
2014-02-16 00:38:47 +01:00
goofy-bz
47aa06d3b0 Update fr.pm
typo fix
2014-02-16 00:26:59 +01:00
44 changed files with 5627 additions and 434 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ lutim.db
script/hypnotoad.pid
local/*
files/*
templates/data.html.ep

28
Changes Normal file
View File

@@ -0,0 +1,28 @@
Revision history for LUTIm
0.2 2014-03-07
- Server-side encryption available
- Thumbnails of uploaded images in response
- Bugfixes
- HTML validity
- Stats (via cron stats command)
- Anonymize IP in DB after a delay (via cron cleanbdd command)
- Watch files directory size (via cron watch command)
- Anonymize logs (log only the senders' IP address)
- Favicon and logo
- Better MIME type detection
- Broadcast message on all pages available
- File max size configurable
- Progress bar
- More options for suppression delay
- Updated documentation
- Cross-domain API
- Upload by image URL
- Add HTTP headers Expires and Content-Cache
0.1 2014-02-15
- Image viewing link
- Image downloading link
- Image twitter card link
- Shutter Plugin
- Configurable "Hosted by" information

141
README.md
View File

@@ -5,18 +5,40 @@ It means Let's Upload That Image.
##What does it do?
It stores images and allows you to see them or download them.
Images are indefinitly stored unless you request that they will be deleted at first view or after 24 hours.
Images are indefinitly stored unless you request that they will be deleted at first view or after 24 hours / one week / one month / one year.
##License
LUTIm is licensed under the terms of the AGPL. See the LICENSE file.
##Dependancies
* Carton : Perl dependancies manager, it will get what you need, so don't bother for dependancies (but you can read the file `cpanfile` if you want).
##Official instance
You can see it working at http://lut.im.
##Logo
LUTIm's logo is an adaptation of [Lutin](http://commons.wikimedia.org/wiki/File:Lutin_by_godo.jpg) by [Godo](http://godoillustrateur.wordpress.com/), licensed under the terms of the CC-BY-SA 3.0 license.
![LUTIm's logo](http://lut.im/img/LUTIm_small.png)
##Dependencies
* Carton : Perl dependencies manager, it will get what you need, so don't bother for dependencies (but you can read the file `cpanfile` if you want).
```shell
sudo cpan Carton
```
or
```shell
sudo apt-get install carton
```
###Thumbnails dependancy
If you want to provide thumbnails of uploaded images, you have to install the *ImageMagick* image manipulation software (<http://www.imagemagick.org/>) and the Image::Magick CPAN module.
On Debian, you can do:
```shell
sudo apt-get install perlmagick
```
##Installation
After installing Carton :
```shell
@@ -24,13 +46,41 @@ git clone https://github.com/ldidry/lutim.git
cd lutim
carton install
cp lutim.conf.template lutim.conf
vi lutim.conf
```
##Configuration
* hypnotoad: listen to listen to, user and group which runs hypnotoad ;
* contact: write something which make people able to contact you (contact form URL, email address, whatever) ;
* secrets: an array of random string. Used by Mojolicious for encrypting session cookies.
* piwik\_img: the Piwik image provides you records of visits without javascript (better privacy than js and cookies) ;
* length: length of the random string part of image's URL (default is 8) ;
* provis\_step: LUTIm provisions random strings for image's URL per pack of `provis_step` (default is 5) ;
* provisioning: number of random strings to provision (default is 100) ;
* hosted\_by: if someone hosts your LUTIm instance, you can add some HTML (a logo for example) to make it appear on index page ;
* tweet\_card\_via: a Twitter account which will appear on Twitter cards ;
* max\_file\_size: well, this is explicit (default is 10Mio = 10485760 octets) ;
* https: 1 if you want to provide secure images URLs (default is 0) ;
* stats\_day\_num: when you generate statistics with `script/lutim cron stats`, you will have stats for the last `stats_day_num` days (default is 365) ;
* keep\_ip\_during: when you delete IP addresses of image's senders with `script/lutim cron cleanbdd`, the IP addresses of images older than `keep_ip_during` days will be deleted (default is 365) ;
* broadcast\_message: put some string (not HTML) here and this message will be displayed on all LUTIm pages (not in JSON responses) ;
* allowed\_domains: array of authorized domains for API calls. Example: `['http://1.example.com', 'http://2.example.com']`. If you want to authorize everyone to use the API: `['*']`.
* default\_delay: what is the default time limit for files? Valid values are 0, 1, 7, 30 and 365;
* max\_delay: if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max\_delay) option and a warning message will be displayed on homepage;
# always\_encrypt: if set to 1, all images will be encrypted.
##Usage
```
carton exec hypnotoad script/lutim
```
##Update
```
git pull
carton install
carton exec hypnotoad script/lutim
```
Yup, that's all (Mojolicious magic), it will listen at "http://127.0.0.1:8080".
For more options (interfaces, user, etc.), change the configuration in `lutim.conf` (have a look at http://mojolicio.us/perldoc/Mojo/Server/Hypnotoad#SETTINGS for the available options).
@@ -50,11 +100,82 @@ sub vcl_recv {
}
if (req.http.host == "lut.im") {
set req.backend = lutim;
return(pass);
}
}
```
##Cron jobs
LUTIm have commands which can be used in cron jobs.
To see what commands are available:
```shell
carton exec script/lutim cron
```
###Statistics
To generate statistics which can be viewed at the address `/stats` (we need to reload hypnotoad after the stats generation):
```shell
carton exec script/lutim cron stats && carton exec hypnotoad script/lutim
```
###Delete IP adresses from database
To automatically delete the IP addresses of image's senders after a configurable delay:
```shell
carton exec script/lutim cron cleanbdd
```
###Delete expired files
To automatically delete files which availability delay is over (when you choose that your image will be deleted after 24h / one week / etc.)
```shell
carton exec script/lutim cron cleanfiles
```
###Watch the size of the files directory
To execute an action when the files directory is heavier than `max_total_size`.
The available actions are `warn` and `stop-upload`:
* `warn` prints a message on the standard out (which is normally mailed to you by `cron`) ;
* `stop-upload` prints a message on the standard out and creates the `stop-upload` file which prevents uploading and put a warn on LUTIm interface ;
* **DANGEROUS OPTION!!!** `delete` prints a message on the standard out and delete older images until the files directory goes under quota.
If the files directory go under quota, the `stop-upload` file is deleted. If you want to manually prevents uploading, create a file named `stop-upload.manual`.
```shell
carton exec script/lutim cron watch
```
##Broadcast message
Set a string in the `broadcast_message` option of `lutim.conf` and reload the server with:
```shell
carton exec hypnotoad script/lutim
```
It may take a few reloads of page before the message is displayed.
##Encryption
LUTIm do encryption on the server if asked to, but does not store the key.
The encryption is made on the server since LUTIm is made to be usable even without javascript. If you want to add client-side encryption for javascript-enabled browsers, patches are welcome.
##API
You can add images by using the API. Here's the parameters of the `POST` request to `/` adress:.
* format: json
MANDATORY if you want to get a json response, otherwise it will send a web page
* file: the image file
MANDATORY
* delete-day: number of days you want the image to stay
OPTIONAL if 0, it will be available undefinitely
* first-view: 1
OPTIONAL if not 0, the image will be deleted at first view
Exemple with curl:
```shell
curl -F "format=json" -F "file=@/tmp/snap0001.jpg" http://lut.im
```
You can allow people to use your instance of LUTIm from other domains.
Add the allowed domains as an array in the `allowed_domains` conf option. Put '`[*]`' if you want to allow all domains.
##Shutter integration
See where Shutter (<http://en.wikipedia.org/wiki/Shutter_%28software%29>) keeps its plugins on your computer.
On my computer, it's in `/usr/share/shutter/resources/system/upload_plugins/upload`.
@@ -69,9 +190,9 @@ And restart Shutter if it was running.
Of course, this plugin is configured for the official instance of LUTIm (<http://lut.im>), feel free to edit it for your own instance.
##Internationalization
LUTIm comes with english and french languages. It will choose the language to display from the browser's settings.
LUTIm comes with English and French languages. It will choose the language to display from the browser's settings.
If you want to add more languages, for example german:
If you want to add more languages, for example German:
```shell
cd lib/I18N
cp en.pm de.pm
@@ -81,7 +202,9 @@ vim de.pm
There's just a few sentences, so it will be quick to translate. Please consider to send me you language file in order to help the other users :smile:.
##Others projects dependancies
LUTIm is written in Perl with the Mojolicious framework, uses the Twitter bootstrap framework to look not too ugly, JQuery and JQuery File Uploader (<https://github.com/danielm/uploader/>) to add some modernity.
LUTIm is written in Perl with the [Mojolicious](http://mojolicio.us) framework, uses the [Twitter bootstrap](http://getbootstrap.com) framework to look not too ugly, [JQuery](http://jquery.com) and [JQuery File Uploader](https://github.com/danielm/uploader/) (slightly modified) to add some modernity, [Raphaël](http://raphaeljs.com/) and [SimpleGraph](http://benaskins.github.io/simplegraph/) for stats graphs.
##Official instance
You can see it working at http://lut.im.
##Contributors
* Luc Didry, aka Sky (<http://www.fiat-tux.fr>), main developer
* Jean-Bernard Marcon, aka Goofy (<https://github.com/goofy-bz>)
* Jean-Christophe Bach (<https://github.com/jcb>)

View File

@@ -362,10 +362,13 @@
"css": [
"print.less",
"type.less",
"grid.less",
"tables.less",
"forms.less",
"buttons.less",
"input-groups.less",
"alerts.less",
"progress-bars.less",
"close.less",
"component-animations.less",
"modals.less",

View File

@@ -2,5 +2,11 @@ requires 'Mojolicious';
requires 'Data::Validate::URI';
requires 'Mojolicious::Plugin::I18N';
requires 'ORLite';
requires 'MIME::Types';
requires 'File::Type';
requires 'Text::Unidecode';
requires 'DateTime';
requires 'Filesys::DiskUsage';
requires 'Switch';
requires 'Data::Validate::URI';
requires 'Crypt::CBC';
requires 'Crypt::Blowfish';

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
package Lutim;
use Mojo::Base 'Mojolicious';
use LutimModel;
use MIME::Types 'by_suffix';
use Mojo::Util qw(quote);
use Mojo::JSON;;
use Digest::file qw(digest_file_hex);
use Text::Unidecode;
use LutimModel;
use Crypt::CBC;
$ENV{MOJO_TMPDIR} = 'tmp';
mkdir($ENV{MOJO_TMPDIR}, 0700) unless (-d $ENV{MOJO_TMPDIR});
# This method will run once at server start
sub startup {
my $self = shift;
@@ -16,16 +15,22 @@ sub startup {
my $config = $self->plugin('Config');
# Default values
$config->{provisionning} = 100 unless (defined($config->{provisionning}));
$config->{provis_step} = 5 unless (defined($config->{provis_step}));
$config->{length} = 8 unless (defined($config->{length}));
$config->{provisioning} = 100 unless (defined($config->{provisionning}));
$config->{provisioning} = 100 unless (defined($config->{provisioning}));
$config->{provis_step} = 5 unless (defined($config->{provis_step}));
$config->{length} = 8 unless (defined($config->{length}));
$config->{always_encrypt} = 0 unless (defined($config->{always_encrypt}));
die "You need to provide a contact information in lutim.conf !" unless (defined($config->{contact}));
$ENV{MOJO_MAX_MESSAGE_SIZE} = $config->{max_file_size} if (defined($config->{max_file_size}));
$self->secrets($config->{secrets});
$self->helper(
render_file => sub {
my $c = shift;
my ($filename, $path, $mediatype, $dl) = @_;
my ($filename, $path, $mediatype, $dl, $expires, $nocache, $key) = @_;
$filename = quote($filename);
@@ -37,13 +42,28 @@ sub startup {
);
return 500;
}
$asset = Mojo::Asset::File->new(path => $path);
$mediatype =~ s/x-//;
my $headers = Mojo::Headers->new();
if ($nocache) {
$headers->add('Cache-Control' => 'no-cache');
} else {
$headers->add('Expires' => $expires);
}
$headers->add('Content-Type' => $mediatype.';name='.$filename);
$headers->add('Content-Disposition' => $dl.';filename='.$filename);
$headers->add('Content-Length' => $asset->size);
$c->res->content->headers($headers);
$c->app->log->debug($key);
if ($key) {
$asset = $c->decrypt($key, $path);
} else {
$asset = Mojo::Asset::File->new(path => $path);
}
$c->res->content->asset($asset);
$headers->add('Content-Length' => $asset->size);
return $c->rendered(200);
}
);
@@ -51,23 +71,29 @@ sub startup {
$self->helper(
ip => sub {
my $c = shift;
my @ip = ($c->tx->remote_address eq '127.0.0.1' && $c->app->mode eq 'production') ? $c->tx->req->{content}->{headers}->{headers}->{'x-forwarded-for'}->[0]->[0] : ($c->tx->remote_address);
return join(',', @ip);
my $proxy = '';
my @x_forward = $c->req->headers->header('X-Forwarded-For');
for my $x (@x_forward) {
$proxy .= join(', ', @$x);
}
my $ip = ($proxy) ? $proxy : $c->tx->remote_address;
return $ip.' Remote port: '.$c->tx->remote_port;
}
);
$self->helper(
provisionning => sub {
provisioning => sub {
my $c = shift;
# Create some short patterns for provisionning
if (LutimModel::Lutim->count('WHERE path IS NULL') < $c->config->{provisionning}) {
# Create some short patterns for provisioning
if (LutimModel::Lutim->count('WHERE path IS NULL') < $c->config->{provisioning}) {
for (my $i = 0; $i < $c->config->{provis_step}; $i++) {
if (LutimModel->begin) {
my $short;
do {
$short= $c->shortener($c->config->{length});
} while (LutimModel::Lutim->count('WHERE short = ?', $short));
} while (LutimModel::Lutim->count('WHERE short = ?', $short) || $short eq 'about' || $short eq 'stats');
LutimModel::Lutim->create(
short => $short,
@@ -97,190 +123,180 @@ sub startup {
}
);
$self->helper(
stop_upload => sub {
my $c = shift;
if (-f 'stop-upload' || -f 'stop-upload.manual') {
$c->stash(
stop_upload => $c->l('stop_upload', $config->{contact})
);
return 1;
}
return 0;
}
);
$self->helper(
max_delay => sub {
my $c = shift;
if (defined($c->config->{max_delay})) {
my $delay = $c->config->{max_delay};
if ($delay >= 0) {
return $delay;
} else {
warn "max_delay set to a negative value. Default to 0."
}
}
return 0;
}
);
$self->helper(
default_delay => sub {
my $c = shift;
if (defined($c->config->{default_delay})) {
my $delay = $c->config->{default_delay};
if ($delay >= 0) {
return $delay;
} else {
warn "default_delay set to a negative value. Default to 0."
}
}
return 0;
}
);
$self->helper(
is_selected => sub {
my $c = shift;
my $num = shift;
return ($num == $c->default_delay) ? 'selected="selected"' : '';
}
);
$self->helper(
crypt => sub {
my $c = shift;
my $upload = shift;
my $filename = shift;
my $key = $c->shortener(8);
my $cipher = Crypt::CBC->new(
-key => $key,
-cipher => 'Blowfish',
-header => 'none',
-iv => 'dupajasi'
);
$cipher->start('encrypting');
my $crypt_asset = Mojo::Asset::File->new;
$crypt_asset->add_chunk($cipher->crypt($upload->slurp));
$crypt_asset->add_chunk($cipher->finish);
my $crypt_upload = Mojo::Upload->new;
$crypt_upload->filename($filename);
$crypt_upload->asset($crypt_asset);
return ($crypt_upload, $key);
}
);
$self->helper(
decrypt => sub {
my $c = shift;
my $key = shift;
my $file = shift;
my $cipher = Crypt::CBC->new(
-key => $key,
-cipher => 'Blowfish',
-header => 'none',
-iv => 'dupajasi'
);
$cipher->start('decrypting');
my $decrypt_asset = Mojo::Asset::File->new;
open(my $f, "<",$file) or die "Unable to read encrypted file: $!";
binmode $f;
while (read($f, my $buffer,1024)) {
$decrypt_asset->add_chunk($cipher->crypt($buffer));
}
$decrypt_asset->add_chunk($cipher->finish) ;
return $decrypt_asset;
}
);
$self->hook(
before_dispatch => sub {
my $c = shift;
$c->stop_upload();
if (defined($c->config->{allowed_domains})) {
if ($c->config->{allowed_domains}->[0] eq '*') {
$c->res->headers->header('Access-Control-Allow-Origin' => '*');
} elsif (my $origin = $c->req->headers->origin) {
for my $domain ($c->config->{allowed_domains}) {
if ($domain->[0] eq $origin) {
$c->res->headers->header('Access-Control-Allow-Origin' => $origin);
last;
}
}
}
}
}
);
$self->hook(
after_dispatch => sub {
shift->provisioning();
}
);
$self->defaults(layout => 'default');
$self->provisionning();
$self->provisioning();
# Router
my $r = $self->routes;
$r->get('/' => sub {
my $c = shift;
$r->options(sub {
my $c = shift;
$c->res->headers->allow('POST') if (defined($c->config->{allowed_domains}));
$c->render(data => '', status => 204);
});
$c->render( template => 'index');
$r->get('/')->
to('Controller#home')->
name('index');
# Check provisionning
$c->on(finish => sub {
shift->provisionning();
}
);
}
)->name('index');
$r->get('/about')->
to('Controller#about')->
name('about');
$r->post('/' => sub {
my $c = shift;
my $upload = $c->param('file');
$r->get('/stats')->
to('Controller#stats')->
name('stats');
my ($mediatype, $encoding) = by_suffix $upload->filename;
$r->post('/')->
to('Controller#add')->
name('add');
my $ip = $c->ip;
$r->get('/:short')->
to('Controller#short')->
name('short');
my ($msg, $short);
# Check file type
if (index($mediatype, 'image') >= 0) {
# Create directory if needed
mkdir('files', 0700) unless (-d 'files');
if(LutimModel->begin) {
my @records = LutimModel::Lutim->select('WHERE path IS NULL LIMIT 1');
if (scalar(@records)) {
# Save file and create record
my $filename = unidecode($upload->filename);
my $ext = ($filename =~ m/([^.]+)$/)[0];
my $path = 'files/'.$records[0]->short.'.'.$ext;
$upload->move_to($path);
$records[0]->update(
path => $path,
filename => $filename,
mediatype => $mediatype,
footprint => digest_file_hex($path, 'SHA-512'),
enabled => 1,
delete_at_day => ($c->param('delete-day')) ? 1 : 0,
delete_at_first_view => ($c->param('first-view')) ? 1 : 0,
created_at => time(),
created_by => $ip
);
# Log image creation
$c->app->log->info('[CREATION] '.$ip.' pushed '.$filename.' (path: '.$path.')');
# Give url to user
$short = $records[0]->short;
} else {
# Houston, we have a problem
$msg = $c->l('no_more_short', $c->config->{contact});
}
}
LutimModel->commit;
} else {
$msg = $c->l('no_valid_file', $upload->filename);
}
# Check provisionning
$c->on(finish => sub {
shift->provisionning();
}
);
if (defined($c->param('format')) && $c->param('format') eq 'json') {
if (defined($short)) {
$msg = {
filename => $upload->filename,
short => $short
};
} else {
$msg = {
filename => $upload->filename,
msg => $msg
};
}
$c->render(
json => {
success => (defined($short)) ? Mojo::JSON->true : Mojo::JSON->false,
msg => $msg
}
);
} else {
$c->flash(msg => $msg) if (defined($msg));
$c->flash(short => $short) if (defined($short));
$c->flash(filename => $upload->filename);
$c->redirect_to('/');
}
}
)->name('add');
$r->get('/:short' => sub {
my $c = shift;
my $short = $c->param('short');
my $touit = $c->param('t');
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
my @images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 1 AND path IS NOT NULL', $short);
my $ip = $c->ip;
if (scalar(@images)) {
if($images[0]->delete_at_day && $images[0]->created_at + 86400 <= time()) {
# Log deletion
$c->app->log->info('[DELETION] '.$ip.' tried to view '.$images[0]->filename.' but it has been removed by expiration (path: '.$images[0]->path.')');
# Delete image
unlink $images[0]->path();
$images[0]->update(enabled => 0);
# Warn user
$c->flash(
msg => $c->l('image_not_found')
);
return $c->redirect_to('/');
}
my $test;
if (defined($touit)) {
$test = 1;
$c->render(
template => 'twitter',
layout => undef,
short => $images[0]->short,
filename => $images[0]->filename
);
} else {
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl);
}
if ($test != 500) {
# Update counter and check provisionning
$c->on(finish => sub {
# Log access
$c->app->log->info('[VIEW] '.$ip.' viewed '.$images[0]->filename.' (path: '.$images[0]->path.')');
# Update record
my $counter = $images[0]->counter + 1;
$images[0]->update(counter => $counter);
$images[0]->update(last_access_at => time());
$images[0]->update(last_access_by => $ip);
# Delete image if needed
if ($images[0]->delete_at_first_view) {
# Log deletion
$c->app->log->info('[DELETION] '.$ip.' made '.$images[0]->filename.' removed (path: '.$images[0]->path.')');
# Delete image
unlink $images[0]->path();
$images[0]->update(enabled => 0);
}
shift->provisionning();
});
}
} else {
@images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 0 AND path IS NOT NULL', $short);
if (scalar(@images)) {
# Log access try
$c->app->log->info('[NOT FOUND] '.$ip.' tried to view '.$short.' but it does\'nt exist.');
# Warn user
$c->flash(
msg => $c->l('image_not_found')
);
return $c->redirect_to('/');
} else {
# Image never existed
$c->render_not_found;
}
}
})->name('short');
$r->get('/:short/:key')->
to('Controller#short');
}
1;

321
lib/Lutim/Controller.pm Normal file
View File

@@ -0,0 +1,321 @@
# vim:set sw=4 ts=4 sts=4 expandtab:
package Lutim::Controller;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Util qw(url_unescape b64_encode);
use DateTime;
use File::Type;
use Digest::file qw(digest_file_hex);
use Text::Unidecode;
use Data::Validate::URI qw(is_http_uri is_https_uri);
use vars qw($im_loaded);
BEGIN {
eval "use Image::Magick";
if ($@) {
warn "You don't have Image::Magick installed so you won't have thumbnails.";
$im_loaded = 0;
} else {
$im_loaded = 1;
}
}
sub home {
my $c = shift;
$c->render(
template => 'index',
max_file_size => $c->req->max_message_size
);
$c->on(finish => sub {
my $c = shift;
$c->app->log->info('[HIT] someone visited site index');
}
);
}
sub about {
shift->render(template => 'about');
}
sub stats {
shift->render(
template => 'stats',
total => LutimModel::Lutim->count('WHERE path IS NOT NULL')
);
}
sub add {
my $c = shift;
my $upload = $c->param('file');
my $file_url = $c->param('lutim-file-url');
if(!defined($c->stash('stop_upload'))) {
if (defined($file_url) && $file_url) {
if (is_http_uri($file_url) || is_https_uri($file_url)) {
my $ua = Mojo::UserAgent->new;
my $tx = $ua->get($file_url => {DNT => 1});
if (my $res = $tx->success) {
$file_url = url_unescape $file_url;
$file_url =~ m#^.*/([^/]*)$#;
my $filename = $1;
$filename = 'uploaded.image' unless (defined($filename));
$filename .= '.image' if (index($filename, '.') == -1);
$upload = Mojo::Upload->new(
asset => $tx->res->content->asset,
filename => $filename
);
} else {
my $msg = $c->l('download_error');
if (defined($c->param('format')) && $c->param('format') eq 'json') {
return $c->render(
json => {
success => Mojo::JSON->false,
msg => {
filename => $file_url,
msg => $msg
}
}
);
} else {
$c->flash(msg => $msg);
$c->flash(filename => $file_url);
return $c->redirect_to('/');
}
}
} else {
my $msg = $c->l('no_valid_url');
if (defined($c->param('format')) && $c->param('format') eq 'json') {
return $c->render(
json => {
success => Mojo::JSON->false,
msg => {
filename => $file_url,
msg => $msg
}
}
);
} else {
$c->flash(msg => $msg);
$c->flash(filename => $file_url);
return $c->redirect_to('/');
}
}
}
my $ft = File::Type->new();
my $mediatype = $ft->mime_type($upload->slurp());
my $ip = $c->ip;
my ($msg, $short, $thumb);
# Check file type
if (index($mediatype, 'image/') >= 0) {
# Create directory if needed
mkdir('files', 0700) unless (-d 'files');
if ($c->req->is_limit_exceeded) {
$msg = l('file_too_big', $c->req->max_message_size);
if (defined($c->param('format')) && $c->param('format') eq 'json') {
return $c->render(
json => {
success => Mojo::JSON->false,
msg => $msg
}
);
} else {
$c->flash(msg => $msg);
$c->flash(filename => $upload->filename);
return $c->redirect_to('/');
}
}
if(LutimModel->begin) {
my @records = LutimModel::Lutim->select('WHERE path IS NULL LIMIT 1');
if (scalar(@records)) {
# Save file and create record
my $filename = unidecode($upload->filename);
my $ext = ($filename =~ m/([^.]+)$/)[0];
my $path = 'files/'.$records[0]->short.'.'.$ext;
if ($im_loaded) {
my $im = Image::Magick->new;
$im->BlobToImage($upload->slurp);
$im->Resize(geometry=>'x85');
$thumb = 'data:'.$mediatype.';base64,';
$thumb .= b64_encode $im->ImageToBlob();
}
my $key;
if ($c->param('crypt') || $c->config->{always_encrypt}) {
($upload, $key) = $c->crypt($upload, $filename);
}
$upload->move_to($path);
$records[0]->update(
path => $path,
filename => $filename,
mediatype => $mediatype,
footprint => digest_file_hex($path, 'SHA-512'),
enabled => 1,
delete_at_day => ($c->param('delete-day')) ? $c->param('delete-day') : $c->max_delay,
delete_at_first_view => ($c->param('first-view')) ? 1 : 0,
created_at => time(),
created_by => $ip
);
# Log image creation
$c->app->log->info('[CREATION] '.$c->ip.' pushed '.$filename.' (path: '.$path.')');
# Give url to user
$short = $records[0]->short;
$short .= '/'.$key if (defined($key));
} else {
# Houston, we have a problem
$msg = $c->l('no_more_short', $c->config->{contact});
}
}
LutimModel->commit;
} else {
$msg = $c->l('no_valid_file', $upload->filename);
}
if (defined($c->param('format')) && $c->param('format') eq 'json') {
if (defined($short)) {
$msg = {
filename => $upload->filename,
short => $short,
thumb => $thumb
};
} else {
$msg = {
filename => $upload->filename,
msg => $msg
};
}
return $c->render(
json => {
success => (defined($short)) ? Mojo::JSON->true : Mojo::JSON->false,
msg => $msg
}
);
} else {
if ((defined($msg))) {
$c->flash(msg => $msg);
$c->flash(filename => $upload->filename);
return $c->redirect_to('/');
} else {
$c->stash(short => $short) if (defined($short));
$c->stash(thumb => $thumb);
$c->stash(filename => $upload->filename);
return $c->render(
template => 'index',
max_file_size => $c->req->max_message_size
);
}
}
} else {
if (defined($c->param('format')) && $c->param('format') eq 'json') {
return $c->render(
json => {
success => Mojo::JSON->false,
msg => {
filename => $upload->filename,
msg => $c->stash('stop_upload')
}
}
);
} else {
$c->flash(msg => $c->stash('stop_upload'));
$c->flash(filename => $upload->filename);
return $c->redirect_to('/');
}
}
}
sub short {
my $c = shift;
my $short = $c->param('short');
my $touit = $c->param('t');
my $key = $c->param('key');
my $dl = (defined($c->param('dl'))) ? 'attachment' : 'inline';
my @images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 1 AND path IS NOT NULL', $short);
if (scalar(@images)) {
if($images[0]->delete_at_day && $images[0]->created_at + $images[0]->delete_at_day * 86400 <= time()) {
# Log deletion
$c->app->log->info('[DELETION] someone tried to view '.$images[0]->filename.' but it has been removed by expiration (path: '.$images[0]->path.')');
# Delete image
unlink $images[0]->path();
$images[0]->update(enabled => 0);
# Warn user
$c->flash(
msg => $c->l('image_not_found')
);
return $c->redirect_to('/');
}
my $test;
if (defined($touit)) {
$test = 1;
my $short = $images[0]->short;
$short .= '/'.$key if (defined($key));
return $c->render(
template => 'twitter',
layout => undef,
short => $short,
filename => $images[0]->filename
);
} else {
my $expires = ($images[0]->delete_at_day) ? $images[0]->delete_at_day : 360;
my $dt = DateTime->from_epoch( epoch => $expires * 86400 + $images[0]->created_at);
$dt->set_time_zone('GMT');
$expires = $dt->strftime("%a, %d %b %Y %H:%M:%S GMT");
$test = $c->render_file($images[0]->filename, $images[0]->path, $images[0]->mediatype, $dl, $expires, $images[0]->delete_at_first_view, $key);
}
if ($test != 500) {
# Update counter
$c->on(finish => sub {
# Log access
$c->app->log->info('[VIEW] someone viewed '.$images[0]->filename.' (path: '.$images[0]->path.')');
# Update record
my $counter = $images[0]->counter + 1;
$images[0]->update(counter => $counter);
$images[0]->update(last_access_at => time());
# Delete image if needed
if ($images[0]->delete_at_first_view) {
# Log deletion
$c->app->log->info('[DELETION] someone made '.$images[0]->filename.' removed (path: '.$images[0]->path.')');
# Delete image
unlink $images[0]->path();
$images[0]->update(enabled => 0);
}
});
}
} else {
@images = LutimModel::Lutim->select('WHERE short = ? AND ENABLED = 0 AND path IS NOT NULL', $short);
if (scalar(@images)) {
# Log access try
$c->app->log->info('[NOT FOUND] someone tried to view '.$short.' but it does\'nt exist.');
# Warn user
$c->flash(
msg => $c->l('image_not_found')
);
return $c->redirect_to('/');
} else {
# Image never existed
$c->render_not_found;
}
}
}
1;

View File

@@ -9,41 +9,69 @@ my $inf_body = <<EOF;
<p>Drag and drop an image in the appropriate area or use the traditional way to send files and LUTIm will provide you two URLs. One to view the image, the other to directly download it.</p>
<p>You can, optionally, request that the image(s) posted on LUTIm to be deleted at first view (or download) or after 24 hours.</p>
<h4>Is it really free (as in free beer)?</h4>
<p>Yes, it is! On the other side, if you want to support the developer, you can do it via <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=LUTIm&category=software">Flattr</a> or with <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
<p>Yes, it is! On the other side, if you want to support the developer, you can do it via <a href="https://flattr.com/submit/auto?user_id=_SKy_&amp;url=[_1]&amp;title=LUTIm&amp;category=software">Flattr</a> or with <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
<h4>Is it really anonymous?</h4>
<p>Yes, it is! On the other side, for legal reasons, your IP address will be stored when you send or view an image. Don't panic, it is the case of all sites on which you go!</p>
<p>The log files containing the IP address of image viewers are retained for one year while the IP addresse of the image's sender, as the address of the last viewer are permanently retained.</p>
<p>Yes, it is! On the other side, for legal reasons, your IP address will be stored when you send an image. Don't panic, it is normally the case of all sites on which you send files!</p>
<p>The IP address of the image's sender is permanently retained.</p>
<p>If the files are deleted if you ask it while posting it, their SHA512 footprint are retained.</p>
<h4>How to report an image?</h4>
<p>Please contact the administrator: [_2]</p>
<h4>How do you pronounce LUTIm?</h4>
<p>Juste like you pronounce the french word <a href="https://fr.wikipedia.org/wiki/Lutin">lutin</a> (/ly.tɛ̃/).</p>
<p>Juste like you pronounce the French word <a href="https://fr.wikipedia.org/wiki/Lutin">lutin</a> (/ly.tɛ̃/).</p>
<h4>What about the software which provides the service?</h4>
<p>The LUTIm software is a <a href="http://en.wikipedia.org/wiki/Free_software">free software</a>, which allows you to download and install it on you own server. Have a look at the <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> to see what you can do.</p>
<p>For more details, see the <a href="https://github.com/ldidry/lutim">Github</a> page of the project.</p>
<h4>Contributors</h4>
<ul>
<li>Luc Didry, aka Sky (<a href="http://www.fiat-tux.fr">http://www.fiat-tux.fr</a>), main developer</li>
<li>Jean-Bernard Marcon, aka Goofy (<a href="https://github.com/goofy-bz">https://github.com/goofy-bz</a>)</li>
<li>Jean-Christophe Bach (<a href="https://github.com/jcb">https://github.com/jcb</a>)</li>
</ul>
EOF
our %Lexicon = (
'license' => 'License:',
'fork-me' => 'Fork me on Github !',
'share-twitter' => 'Share on Twitter',
'informations' => 'Informations',
'informations-body' => $inf_body,
'view-link' => 'View link:',
'download-link' => 'Download link:',
'twitter-link' => 'Link for put in a tweet:',
'some-bad' => 'Something bad happened',
'delete-first' => 'Delete at first view?',
'delete-day' => 'Delete after 24 hours?',
'upload_image' => 'Send an image',
'image-only' => 'Only images are allowed',
'go' => 'Let\'s go!',
'drag-n-drop' => 'Drag & drop images here',
'or' => '-or-',
'file-browser' => 'Click to open the file browser',
'image_not_found' => 'Unable to find the image',
'no_more_short' => 'There is no more available URL. Retry or contact the administrator. [_1]',
'no_valid_file' => 'The file [_1] is not an image.',
'license' => 'License:',
'fork-me' => 'Fork me on Github !',
'share-twitter' => 'Share on Twitter',
'informations' => 'Informations',
'informations-body' => $inf_body,
'view-link' => 'View link',
'download-link' => 'Download link',
'twitter-link' => 'Link for put in a tweet',
'some-bad' => 'Something bad happened',
'delete-first' => 'Delete at first view?',
'delete-day' => 'Delete after 24 hours?',
'upload_image' => 'Send an image',
'image-only' => 'Only images are allowed',
'go' => 'Let\'s go!',
'drag-n-drop' => 'Drag & drop images here',
'or' => '-or-',
'file-browser' => 'Click to open the file browser',
'image_not_found' => 'Unable to find the image',
'no_more_short' => 'There is no more available URL. Retry or contact the administrator. [_1]',
'no_valid_file' => 'The file [_1] is not an image.',
'file_too_big' => 'The file exceed the size limit ([_1])',
'no_time_limit' => 'No time limit',
'24_hours' => '24 hours',
'7_days' => '7 days',
'30_days' => '30 days',
'1_year' => 'One year',
'pushed-images' => ' sent images on this instance from beginning.',
'graph-data-once-a-day' => 'The graph\'s datas are not updated in real-time.',
'lutim-stats' => 'LUTIm\'s statistics',
'back-to-index' => 'Back to homepage',
'stop_upload' => 'Uploading is currently disabled, please try later or contact the administrator ([_1]).',
'download_error' => 'An error occured while downloading the image.',
'no_valid_url' => 'The URL is not valid.',
'image_url' => 'Image URL',
'upload_image_url' => 'Upload an image with its URL',
'delay_0' => 'no time limit',
'delay_1' => '24 hours',
'delay_days' => '[_1] days',
'delay_365' => '1 year',
'max_delay' => 'Warning! The maximum time limit for an image is [_1] day(s), even if you choose "no time limit".',
'crypt_image' => 'Encrypt the image (LUTIm does not keep the key).',
'always_encrypt' => 'The images are encrypted on the server (LUTIm does not keep the key).',
);
1;

View File

@@ -6,13 +6,13 @@ my $inf_body = <<EOF;
<p>LUTIm est un service gratuit et anonyme dhébergement dimages. Il sagit aussi du nom du logiciel (libre) qui fournit ce service.</p>
<p>Les images déposées sur LUTIm peuvent être stockées indéfiniment, ou seffacer dès le premier affichage ou au bout de 24h.</p>
<h4>Comment ça marche ?</h4>
<p>Faites glisser des images dans la zone prévue à cette effet ou sélectionnez un fichier de façon classique et LUTIm vous fournira deux URLs en retour. Une pour afficher limage, lautre pour la télécharger directement.</p>
<p>Vous pouvez, de façon optionnelle, demander à ce que la ou les images déposées sur LUTIm soient supprimées après leur premier affichage (ou téléchargement) ou au bout de 24 heures.</p>
<p>Faites glisser des images dans la zone prévue à cet effet ou sélectionnez un fichier de façon classique et LUTIm vous fournira deux URLs en retour. Une pour afficher limage, lautre pour la télécharger directement.</p>
<p>Vous pouvez, de façon facultative, demander à ce que la ou les images déposées sur LUTIm soient supprimées après leur premier affichage (ou téléchargement) ou au bout de 24 heures.</p>
<h4>Cest vraiment gratuit ?</h4>
<p>Oui, ça lest ! Par contre, si vous avez envie de soutenir le développeur, vous pouvez faire un microdon avec <a href="https://flattr.com/submit/auto?user_id=_SKy_&url=[_1]&title=LUTIm&category=software">Flattr</a> ou en <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
<p>Oui, ça lest ! Par contre, si vous avez envie de soutenir le développeur, vous pouvez faire un microdon avec <a href="https://flattr.com/submit/auto?user_id=_SKy_&amp;url=[_1]&amp;title=LUTIm&amp;category=software">Flattr</a> ou en <a href="bitcoin:1K3n4MXNRSMHk28oTfXEvDunWFthePvd8v?label=lutim">BitCoin</a>.</p>
<h4>Cest vraiment anonyme ?</h4>
<p>Oui, ça lest ! Par contre, pour des raisons légales, votre adresse IP sera enregistrée lorsque vous enverrez ou consulterez une image.Ne vous affolez pas, cest de toute façon le cas sur tous les sites sur lesquels vous surfez !</p>
<p>Les journaux systèmes contenant ladresse IP des visiteurs dune image sont conservés un an, tandis que lIP de la personne ayant déposé limage et celle du dernier visiteur de limage sont stockées de manière définitive.</p>
<p>Oui, ça lest ! Par contre, pour des raisons légales, votre adresse IP sera enregistrée lorsque vous enverrez une image. Ne vous affolez pas, cest de toute façon normalement le cas de tous les sites sur lesquels vous envoyez des fichiers !</p>
<p>LIP de la personne ayant déposé limage est stockée de manière définitive.</p>
<p>Si les fichiers sont bien supprimés si vous en avez exprimé le choix, leur empreinte SHA512 est toutefois conservée.</p>
<h4>Comment peut-on faire pour signaler une image ?</h4>
<p>Veuillez contacter ladministrateur : [_2]</p>
@@ -21,29 +21,57 @@ my $inf_body = <<EOF;
<h4>Et à propos du logiciel qui fournit le service ?</h4>
<p>Le logiciel LUTIm est un <a href="https://fr.wikipedia.org/wiki/Logiciel_libre">logiciel libre</a>, ce qui vous permet de le télécharger et de linstaller sur votre propre serveur. Jetez un coup dœil à l<a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL</a> pour voir quels sont vos droits.</p>
<p>Pour plus de détails, consultez la page <a href="https://github.com/ldidry/lutim">Github</a> du projet.</p>
<h4>Contributeurs</h4>
<ul>
<li>Luc Didry, aka Sky (<a href="http://www.fiat-tux.fr">http://www.fiat-tux.fr</a>), développeur principal</li>
<li>Jean-Bernard Marcon, aka Goofy (<a href="https://github.com/goofy-bz">https://github.com/goofy-bz</a>)</li>
<li>Jean-Christophe Bach (<a href="https://github.com/jcb">https://github.com/jcb</a>)</li>
</ul>
EOF
our %Lexicon = (
'license' => 'Licence :',
'fork-me' => 'Fork me on Github',
'share-twitter' => 'Partager sur Twitter',
'informations' => 'Informations',
'informations-body' => $inf_body,
'view-link' => 'Lien d\'affichage :',
'download-link' => 'Lien de téléchargement :',
'twitter-link' => 'Lien pour mettre dans un tweet :',
'some-bad' => 'Un problème est survenu',
'delete-first' => 'Supprimer au premier accès ?',
'delete-day' => 'Supprimer après 24 heures ?',
'upload_image' => 'Envoyez une image',
'image-only' => 'Seules les images sont acceptées',
'go' => 'Allons-y !',
'drag-n-drop' => 'Déposez vos images ici',
'or' => '-ou-',
'file-browser' => 'Cliquez pour utiliser le navigateur de fichier',
'image_not_found' => 'Impossible de trouver l\'image',
'no_more_short' => 'Il n\'y a plus d\'URL disponible. Veuillez réessayer ou contactez l\'administrateur. [_1].',
'no_valid_file' => 'Le fichier [_1] n\'est pas une image.'
'license' => 'Licence :',
'fork-me' => 'Fork me on Github',
'share-twitter' => 'Partager sur Twitter',
'informations' => 'Informations',
'informations-body' => $inf_body,
'view-link' => 'Lien d\'affichage',
'download-link' => 'Lien de téléchargement',
'twitter-link' => 'Lien pour mettre dans un tweet',
'some-bad' => 'Un problème est survenu',
'delete-first' => 'Supprimer au premier accès ?',
'delete-day' => 'Supprimer après 24 heures ?',
'upload_image' => 'Envoyez une image',
'image-only' => 'Seules les images sont acceptées',
'go' => 'Allons-y !',
'drag-n-drop' => 'Déposez vos images ici',
'or' => '-ou-',
'file-browser' => 'Cliquez pour utiliser le navigateur de fichier',
'image_not_found' => 'Impossible de trouver l\'image',
'no_more_short' => 'Il n\'y a plus d\'URL disponible. Veuillez réessayer ou contactez l\'administrateur. [_1].',
'no_valid_file' => 'Le fichier [_1] n\'est pas une image.',
'file_too_big' => 'Le fichier dépasse la limite de taille ([_1])',
'no_time_limit' => 'Pas de limitation de durée',
'24_hours' => '24 heures',
'7_days' => '7 jours',
'30_days' => '30 jours',
'1_year' => 'Un an',
'pushed-images' => ' images envoyées sur cette instance depuis le début.',
'graph-data-once-a-day' => 'Les données du graphique ne sont pas mises à jour en temps réél.',
'lutim-stats' => 'Statistiques de LUTIm',
'back-to-index' => 'Retour à la page d\'accueil',
'stop_upload' => 'L\'envoi d\'images est actuellement désactivé, veuillez réessayer plus ou contacter l\'administrateur ([_1]).',
'download_error' => 'Une erreur est survenue lors du téléchargement de l\'image.',
'no_valid_url' => 'l\'URL n\'est pas valide.',
'image_url' => 'URL de l\'image',
'upload_image_url' => 'Déposer une image par son URL',
'delay_0' => 'pas de limitation de durée',
'delay_1' => '24 heures',
'delay_days' => '[_1] jours',
'delay_365' => '1 an',
'max_delay' => 'Attention ! Le délai maximal de rétention d\'une image est de [_1] jour(s), même si vous choisissez « pas de limitation de durée ».',
'crypt_image' => 'Chiffrer l\'image (LUTIm ne stocke pas la clé).',
'always_encrypt' => 'Les images sont chiffrées sur le serveur (LUTIm ne stocke pas la clé).',
);
1;

View File

@@ -0,0 +1,26 @@
package Mojolicious::Command::cron;
use Mojo::Base 'Mojolicious::Commands';
has description => 'Execute tasks.';
has hint => <<EOF;
See 'script/lutim cron help TASK' for more information on a specific task.
EOF
has message => sub { shift->extract_usage . "\nCron tasks:\n" };
has namespaces => sub { ['Mojolicious::Command::cron'] };
sub help { shift->run(@_) }
1;
=encoding utf8
=head1 NAME
Mojolicious::Command::cron - Cron commands
=head1 SYNOPSIS
Usage: script/lutim cron TASK [OPTIONS]
=cut

View File

@@ -0,0 +1,38 @@
package Mojolicious::Command::cron::cleanbdd;
use Mojo::Base 'Mojolicious::Command';
use LutimModel;
use Mojo::Util qw(slurp decode);
use Mojolicious::Plugin::Config;
has description => 'Delete IP addresses from database after configured delay.';
has usage => sub { shift->extract_usage };
sub run {
my $c = shift;
my $config = Mojolicious::Plugin::Config->parse(decode('UTF-8', slurp 'lutim.conf'), 'lutim.conf');
$config->{keep_ip_during} = (defined($config->{keep_ip_during})) ? $config->{keep_ip_during} : 365;
my $separation = time() - $config->{keep_ip_during} * 86400;
LutimModel->do(
'UPDATE lutim SET created_by = "" WHERE path IS NOT NULL AND created_at < ?',
{},
$separation
);
}
=encoding utf8
=head1 NAME
Mojolicious::Command::cron::cleanbdd - Delete IP addresses from database after configured delay
=head1 SYNOPSIS
Usage: script/lutim cron cleanbdd
=cut
1;

View File

@@ -0,0 +1,36 @@
package Mojolicious::Command::cron::cleanfiles;
use Mojo::Base 'Mojolicious::Command';
use LutimModel;
use Mojo::Util qw(slurp decode);
use Mojolicious::Plugin::Config;
has description => 'Delete expired files.';
has usage => sub { shift->extract_usage };
sub run {
my $c = shift;
my $config = Mojolicious::Plugin::Config->parse(decode('UTF-8', slurp 'lutim.conf'), 'lutim.conf');
my $time = time();
my @images = LutimModel::Lutim->select('WHERE enabled = 1 AND (delete_at_day * 86400) < (? - created_at) AND delete_at_day != 0', $time);
for my $image (@images) {
$image->update(enabled => 0);
unlink $image->path();
}
}
=encoding utf8
=head1 NAME
Mojolicious::Command::cron::cleanfiles - Delete expired files
=head1 SYNOPSIS
Usage: script/lutim cron cleanfiles
=cut
1;

View File

@@ -0,0 +1,64 @@
package Mojolicious::Command::cron::stats;
use Mojo::Base 'Mojolicious::Command';
use LutimModel;
use Mojo::DOM;
use Mojo::Util qw(slurp spurt decode);
use DateTime;
use Mojolicious::Plugin::Config;
has description => 'Generate statistics about LUTIm.';
has usage => sub { shift->extract_usage };
sub run {
my $c = shift;
my $config = Mojolicious::Plugin::Config->parse(decode('UTF-8', slurp 'lutim.conf'), 'lutim.conf');
$config->{stats_day_num} = (defined($config->{stats_day_num})) ? $config->{stats_day_num} : 365;
my $text = slurp('templates/data.html.ep.template');
my $dom = Mojo::DOM->new($text);
my $thead_tr = $dom->at('table thead tr');
my $tbody_tr = $dom->at('table tbody tr');
my $tbody_t2 = $tbody_tr->next;
my $separation = time() - $config->{stats_day_num} * 86400;
my %data;
for my $img (LutimModel::Lutim->select('WHERE path IS NOT NULL AND created_at >= ?', $separation)) {
my $time = DateTime->from_epoch(epoch => $img->created_at);
my ($year, $month, $day) = ($time->year(), $time->month(), $time->day());
if (defined($data{$year}->{$month}->{$day})) {
$data{$year}->{$month}->{$day} += 1;
} else {
$data{$year}->{$month}->{$day} = 1;
}
}
my $total = LutimModel::Lutim->count('WHERE path IS NOT NULL AND created_at < ?', $separation);
for my $year (sort keys %data) {
for my $month (sort keys %{$data{$year}}) {
for my $day (sort keys %{$data{$year}->{$month}}) {
$thead_tr->append_content('<th>'."$day/$month/$year".'</th>'."\n");
$tbody_tr->append_content('<td>'.$data{$year}->{$month}->{$day}.'</td>'."\n");
$total += $data{$year}->{$month}->{$day};
$tbody_t2->append_content('<td>'.$total.'</td>'."\n");
}
}
}
spurt $dom, 'templates/data.html.ep';
}
=encoding utf8
=head1 NAME
Mojolicious::Command::cron::stats - Stats generator
=head1 SYNOPSIS
Usage: script/lutim cron stats
=cut
1;

View File

@@ -0,0 +1,68 @@
package Mojolicious::Command::cron::watch;
use Mojo::Base 'Mojolicious::Command';
use Mojo::Util qw(slurp decode);
use Mojolicious::Plugin::Config;
use Filesys::DiskUsage qw/du/;
use LutimModel;
use Switch;
has description => 'Watch the files directory and take action when over quota';
has usage => sub { shift->extract_usage };
sub run {
my $c = shift;
my $config = Mojolicious::Plugin::Config->parse(decode('UTF-8', slurp 'lutim.conf'), 'lutim.conf');
if (defined($config->{max_total_size})) {
my $total = du(qw/files/);
if ($total > $config->{max_total_size}) {
if (defined($config->{policy_when_full})) {
say "[LUTIm cron job watch] Files directory is over quota ($total > ".$config->{max_total_size}.")";
switch ($config->{policy_when_full}) {
case 'warn' {
say "[LUTIm cron job watch] Please, delete some files or increase quota (".$config->{max_total_size}.")";
}
case 'stop-upload' {
open (my $fh, '>', 'stop-upload') or die ("Couldn't open stop-upload: $!");
close($fh);
say '[LUTIm cron job watch] Uploads are stopped. Delete some images and the stop-upload file to reallow uploads.';
}
case 'delete' {
say '[LUTIm cron job watch] Older files are being deleted';
do {
for my $img (LutimModel::Lutim->select('WHERE path IS NOT NULL AND enabled = 1 ORDER BY created_at ASC LIMIT 50')) {
unlink $img->path() or warn "Could not unlink ".$img->path.": $!";
$img->update(enabled => 0);
}
} while (du(qw/files/) > $config->{max_total_size});
}
else {
say '[LUTIm cron job watch] Unrecognized policy_when_full option: '.$config->{policy_when_full}.'. Aborting.';
}
}
} else {
say "[LUTIm cron job watch] Files directory over quota ($total > ".$config->{max_total_size}.") but no configured policy_when_full option!" ;
}
} else {
unlink 'stop-upload' if (-f 'stop-upload');
}
} else {
say "[LUTIm cron job watch] No max_total_size found in the configuration file. Aborting.";
}
}
=encoding utf8
=head1 NAME
Mojolicious::Command::cron::watch - Delete IP addresses from database after configured delay
=head1 SYNOPSIS
Usage: script/lutim cron watch
=cut
1;

View File

@@ -1,16 +1,28 @@
# vim:set sw=4 ts=4 sts=4 ft=perl expandtab:
# See README.md for more explanations
{
hypnotoad => {
listen => ['http://127.0.0.1:8080'],
user => 'www-data',
group => 'www-data'
},
contact => 'John Doe, admin[at]example.com',
secrets => ['fdjsofjoihrei'], # please provide a random string
piwik_img => 'https://piwik.example.org/piwik.php?idsite=1&rec=1', # optional, only the piwik image tracker is allowed, no javascript
length => 8, # optional
provis_step => 5, # optional
provisionning => 100, # optional
hosted_by => 'My super hoster <img src="http://hoster.example.com" alt="Hoster logo">', # optional
tweet_card_via => '@framasky' # optional
#contact => 'John Doe, admin[at]example.com',
secrets => ['fdjsofjoihrei'], # please provide a random string
length => 8, # optional
provis_step => 5, # optional
provisioning => 100, # optional
tweet_card_via => '@framasky', # optional
max_file_size => 10485760, # optional, size in octets, you can write it 10*1024*1024
#piwik_img => 'https://piwik.example.org/piwik.php?idsite=1&amp;rec=1', # optional, only the piwik image tracker is allowed, no javascript
#hosted_by => 'My super hoster <img src="http://hoster.example.com" alt="Hoster logo">', # optional
#https => 0, # optional, set to 1 if you use Lutim behind a secure web server
#stats_day_num => 365, # optional, number of days shown in /stats page (used with script/lutim cron stats)
#keep_ip_during => 365, # optional, number of days, after that delay, the IP addresses of image creators will be deleted (used with script/lutim cron cleanbdd)
#max_total_size => 10*1024*1024*1024, # optional, maximum total size of the files directory (used with script/lutim cron watch)
#policy_when_full => 'warn', # optional, policy when files directory is over max_total_size. Valid values are 'warn', 'stop-upload' and 'delete'. See README.
#broadcast_message => 'Maintenance', #optional, the broadcast_message is displayed on all pages of LUTIm (but no in json response)
#allowed_domains => ['http://1.example.com', 'http://2.example.com'], #optional, array of authorized domains for API calls. If you want to authorize everyone to use the API: ['*']
#default_delay => 0, #optional: what is the default time limit for files? Valid values are 0, 1, 7, 30 and 365.
#max_delay => 0, #optional, if defined, the images will be deleted after that delay (in days), even if they were uploaded with "no delay" (or value superior to max\_delay) option and a warning message will be displayed on homepage.
#always_encrypt => 0, #optional, if set to 1, all the images will be encrypted
};

View File

@@ -1347,6 +1347,235 @@ address {
margin-left: 0%;
}
}
table {
max-width: 100%;
background-color: transparent;
}
th {
text-align: left;
}
.table {
width: 100%;
margin-bottom: 20px;
}
.table > thead > tr > th,
.table > tbody > tr > th,
.table > tfoot > tr > th,
.table > thead > tr > td,
.table > tbody > tr > td,
.table > tfoot > tr > td {
padding: 8px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #dddddd;
}
.table > thead > tr > th {
vertical-align: bottom;
border-bottom: 2px solid #dddddd;
}
.table > caption + thead > tr:first-child > th,
.table > colgroup + thead > tr:first-child > th,
.table > thead:first-child > tr:first-child > th,
.table > caption + thead > tr:first-child > td,
.table > colgroup + thead > tr:first-child > td,
.table > thead:first-child > tr:first-child > td {
border-top: 0;
}
.table > tbody + tbody {
border-top: 2px solid #dddddd;
}
.table .table {
background-color: #ffffff;
}
.table-condensed > thead > tr > th,
.table-condensed > tbody > tr > th,
.table-condensed > tfoot > tr > th,
.table-condensed > thead > tr > td,
.table-condensed > tbody > tr > td,
.table-condensed > tfoot > tr > td {
padding: 5px;
}
.table-bordered {
border: 1px solid #dddddd;
}
.table-bordered > thead > tr > th,
.table-bordered > tbody > tr > th,
.table-bordered > tfoot > tr > th,
.table-bordered > thead > tr > td,
.table-bordered > tbody > tr > td,
.table-bordered > tfoot > tr > td {
border: 1px solid #dddddd;
}
.table-bordered > thead > tr > th,
.table-bordered > thead > tr > td {
border-bottom-width: 2px;
}
.table-striped > tbody > tr:nth-child(odd) > td,
.table-striped > tbody > tr:nth-child(odd) > th {
background-color: #f9f9f9;
}
.table-hover > tbody > tr:hover > td,
.table-hover > tbody > tr:hover > th {
background-color: #f5f5f5;
}
table col[class*="col-"] {
position: static;
float: none;
display: table-column;
}
table td[class*="col-"],
table th[class*="col-"] {
position: static;
float: none;
display: table-cell;
}
.table > thead > tr > td.active,
.table > tbody > tr > td.active,
.table > tfoot > tr > td.active,
.table > thead > tr > th.active,
.table > tbody > tr > th.active,
.table > tfoot > tr > th.active,
.table > thead > tr.active > td,
.table > tbody > tr.active > td,
.table > tfoot > tr.active > td,
.table > thead > tr.active > th,
.table > tbody > tr.active > th,
.table > tfoot > tr.active > th {
background-color: #f5f5f5;
}
.table-hover > tbody > tr > td.active:hover,
.table-hover > tbody > tr > th.active:hover,
.table-hover > tbody > tr.active:hover > td,
.table-hover > tbody > tr.active:hover > th {
background-color: #e8e8e8;
}
.table > thead > tr > td.success,
.table > tbody > tr > td.success,
.table > tfoot > tr > td.success,
.table > thead > tr > th.success,
.table > tbody > tr > th.success,
.table > tfoot > tr > th.success,
.table > thead > tr.success > td,
.table > tbody > tr.success > td,
.table > tfoot > tr.success > td,
.table > thead > tr.success > th,
.table > tbody > tr.success > th,
.table > tfoot > tr.success > th {
background-color: #dff0d8;
}
.table-hover > tbody > tr > td.success:hover,
.table-hover > tbody > tr > th.success:hover,
.table-hover > tbody > tr.success:hover > td,
.table-hover > tbody > tr.success:hover > th {
background-color: #d0e9c6;
}
.table > thead > tr > td.info,
.table > tbody > tr > td.info,
.table > tfoot > tr > td.info,
.table > thead > tr > th.info,
.table > tbody > tr > th.info,
.table > tfoot > tr > th.info,
.table > thead > tr.info > td,
.table > tbody > tr.info > td,
.table > tfoot > tr.info > td,
.table > thead > tr.info > th,
.table > tbody > tr.info > th,
.table > tfoot > tr.info > th {
background-color: #d9edf7;
}
.table-hover > tbody > tr > td.info:hover,
.table-hover > tbody > tr > th.info:hover,
.table-hover > tbody > tr.info:hover > td,
.table-hover > tbody > tr.info:hover > th {
background-color: #c4e3f3;
}
.table > thead > tr > td.warning,
.table > tbody > tr > td.warning,
.table > tfoot > tr > td.warning,
.table > thead > tr > th.warning,
.table > tbody > tr > th.warning,
.table > tfoot > tr > th.warning,
.table > thead > tr.warning > td,
.table > tbody > tr.warning > td,
.table > tfoot > tr.warning > td,
.table > thead > tr.warning > th,
.table > tbody > tr.warning > th,
.table > tfoot > tr.warning > th {
background-color: #fcf8e3;
}
.table-hover > tbody > tr > td.warning:hover,
.table-hover > tbody > tr > th.warning:hover,
.table-hover > tbody > tr.warning:hover > td,
.table-hover > tbody > tr.warning:hover > th {
background-color: #faf2cc;
}
.table > thead > tr > td.danger,
.table > tbody > tr > td.danger,
.table > tfoot > tr > td.danger,
.table > thead > tr > th.danger,
.table > tbody > tr > th.danger,
.table > tfoot > tr > th.danger,
.table > thead > tr.danger > td,
.table > tbody > tr.danger > td,
.table > tfoot > tr.danger > td,
.table > thead > tr.danger > th,
.table > tbody > tr.danger > th,
.table > tfoot > tr.danger > th {
background-color: #f2dede;
}
.table-hover > tbody > tr > td.danger:hover,
.table-hover > tbody > tr > th.danger:hover,
.table-hover > tbody > tr.danger:hover > td,
.table-hover > tbody > tr.danger:hover > th {
background-color: #ebcccc;
}
@media (max-width: 767px) {
.table-responsive {
width: 100%;
margin-bottom: 15px;
overflow-y: hidden;
overflow-x: scroll;
-ms-overflow-style: -ms-autohiding-scrollbar;
border: 1px solid #dddddd;
-webkit-overflow-scrolling: touch;
}
.table-responsive > .table {
margin-bottom: 0;
}
.table-responsive > .table > thead > tr > th,
.table-responsive > .table > tbody > tr > th,
.table-responsive > .table > tfoot > tr > th,
.table-responsive > .table > thead > tr > td,
.table-responsive > .table > tbody > tr > td,
.table-responsive > .table > tfoot > tr > td {
white-space: nowrap;
}
.table-responsive > .table-bordered {
border: 0;
}
.table-responsive > .table-bordered > thead > tr > th:first-child,
.table-responsive > .table-bordered > tbody > tr > th:first-child,
.table-responsive > .table-bordered > tfoot > tr > th:first-child,
.table-responsive > .table-bordered > thead > tr > td:first-child,
.table-responsive > .table-bordered > tbody > tr > td:first-child,
.table-responsive > .table-bordered > tfoot > tr > td:first-child {
border-left: 0;
}
.table-responsive > .table-bordered > thead > tr > th:last-child,
.table-responsive > .table-bordered > tbody > tr > th:last-child,
.table-responsive > .table-bordered > tfoot > tr > th:last-child,
.table-responsive > .table-bordered > thead > tr > td:last-child,
.table-responsive > .table-bordered > tbody > tr > td:last-child,
.table-responsive > .table-bordered > tfoot > tr > td:last-child {
border-right: 0;
}
.table-responsive > .table-bordered > tbody > tr:last-child > th,
.table-responsive > .table-bordered > tfoot > tr:last-child > th,
.table-responsive > .table-bordered > tbody > tr:last-child > td,
.table-responsive > .table-bordered > tfoot > tr:last-child > td {
border-bottom: 0;
}
}
fieldset {
padding: 0;
margin: 0;
@@ -2315,6 +2544,82 @@ select[multiple].input-group-sm > .input-group-btn > .btn {
.alert-danger .alert-link {
color: #843534;
}
@-webkit-keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
@keyframes progress-bar-stripes {
from {
background-position: 40px 0;
}
to {
background-position: 0 0;
}
}
.progress {
overflow: hidden;
height: 20px;
margin-bottom: 20px;
background-color: #f5f5f5;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-bar {
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 20px;
color: #ffffff;
text-align: center;
background-color: #428bca;
-webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
-webkit-transition: width 0.6s ease;
transition: width 0.6s ease;
}
.progress-striped .progress-bar {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-size: 40px 40px;
}
.progress.active .progress-bar {
-webkit-animation: progress-bar-stripes 2s linear infinite;
animation: progress-bar-stripes 2s linear infinite;
}
.progress-bar-success {
background-color: #5cb85c;
}
.progress-striped .progress-bar-success {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}
.progress-bar-info {
background-color: #5bc0de;
}
.progress-striped .progress-bar-info {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}
.progress-bar-warning {
background-color: #f0ad4e;
}
.progress-striped .progress-bar-warning {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}
.progress-bar-danger {
background-color: #d9534f;
}
.progress-striped .progress-bar-danger {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}
.close {
float: right;
font-size: 21px;

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,8 @@
.icon-eye:before { content: '\e806'; } /* '' */
.icon-download:before { content: '\e804'; } /* '' */
.icon-bitcoin:before { content: '\e802'; } /* '' */
.icon-spinner:before { content: '\e805'; } /* '' */
.icon-github-circled:before { content: '\e800'; } /* '' */
.icon-touiteur:before { content: '\e801'; } /* '' */
.icon-flattr:before { content: '\e803'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,8 @@
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-touiteur { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }

View File

@@ -10,7 +10,10 @@
/* font-size: 120%; */
}
.icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-download { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-bitcoin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-spinner { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-github-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-touiteur { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-flattr { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }

View File

@@ -1,10 +1,10 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?4658339');
src: url('../font/fontello.eot?4658339#iefix') format('embedded-opentype'),
url('../font/fontello.woff?4658339') format('woff'),
url('../font/fontello.ttf?4658339') format('truetype'),
url('../font/fontello.svg?4658339#fontello') format('svg');
src: url('../font/fontello.eot?69050751');
src: url('../font/fontello.eot?69050751#iefix') format('embedded-opentype'),
url('../font/fontello.woff?69050751') format('woff'),
url('../font/fontello.ttf?69050751') format('truetype'),
url('../font/fontello.svg?69050751#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -14,7 +14,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?4658339#fontello') format('svg');
src: url('../font/fontello.svg?69050751#fontello') format('svg');
}
}
*/
@@ -50,7 +50,10 @@
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-eye:before { content: '\e806'; } /* '' */
.icon-download:before { content: '\e804'; } /* '' */
.icon-bitcoin:before { content: '\e802'; } /* '' */
.icon-spinner:before { content: '\e805'; } /* '' */
.icon-github-circled:before { content: '\e800'; } /* '' */
.icon-touiteur:before { content: '\e801'; } /* '' */
.icon-flattr:before { content: '\e803'; } /* '' */

Binary file not shown.

View File

@@ -6,7 +6,10 @@
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="eye" unicode="&#xe806;" d="m929 314q-85 132-213 197q34-58 34-125q0-104-73-177t-177-73t-177 73t-73 177q0 67 34 125q-128-65-213-197q75-114 187-182t242-68t242 68t187 182z m-402 215q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-12 8-19t19-8t19 8t8 19q0 48 34 82t82 34q11 0 19 8t8 19z m473-215q0-19-11-38q-78-129-210-206t-279-77t-279 77t-210 206q-11 19-11 38t11 39q78 128 210 205t279 78t279-78t210-205q11-20 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="download" unicode="&#xe804;" d="m714 100q0 15-10 25t-25 11t-26-11t-10-25t10-25t26-11t25 11t10 25z m143 0q0 15-10 25t-26 11t-25-11t-10-25t10-25t25-11t26 11t10 25z m72 125v-179q0-22-16-37t-38-16h-821q-23 0-38 16t-16 37v179q0 22 16 38t38 16h259l75-76q33-32 76-32t76 32l76 76h259q22 0 38-16t16-38z m-182 318q10-23-8-40l-250-250q-10-10-25-10t-25 10l-250 250q-17 17-8 40q10 21 33 21h143v250q0 15 11 25t25 11h143q14 0 25-11t10-25v-250h143q24 0 33-21z" horiz-adv-x="928.6" />
<glyph glyph-name="bitcoin" unicode="&#xe802;" d="m651 493q10-102-73-144q65-16 98-58t25-119q-4-40-18-70t-36-49t-54-33t-68-19t-81-9v-142h-86v140q-45 0-68 1v-141h-86v142q-10 0-30 1t-31 0h-112l18 102h62q27 0 32 28v225h9q-4 0-9 0v160q-7 38-50 38h-62v92l119-1q35 0 54 1v141h86v-138q45 1 68 1v137h86v-141q44-4 78-13t63-25t46-43t20-64z m-120-304q0 20-8 35t-21 26t-32 17t-36 10t-42 5t-38 2t-36 0t-27-1v-189q5 0 21 0t27 0t29 1t33 2t32 5t31 8t26 11t22 17t14 22t5 29z m-39 265q0 19-7 33t-17 23t-27 16t-31 9t-34 5t-33 1t-30 0t-22-1v-171q3 0 20 0t26 0t27 1t31 3t29 6t27 10t21 15t15 22t5 28z" horiz-adv-x="714.3" />
<glyph glyph-name="spinner" unicode="&#xe805;" d="m277 100q0-33-24-57t-57-23q-33 0-56 23t-24 57t24 57t56 23q33 0 57-23t24-57z m241-107q0-30-21-51t-51-21t-50 21t-21 51t21 50t50 21t51-21t21-50z m-339 357q0-37-27-63t-63-26t-63 26t-26 63t26 63t63 26t63-26t27-63z m580-250q0-26-18-44t-45-18t-44 18t-18 44t18 44t44 19t45-19t18-44z m-464 500q0-41-29-69t-70-29t-69 29t-29 69t29 69t69 29t70-29t29-69z m259 107q0-45-32-76t-76-31t-75 31t-32 76t32 76t75 31t76-31t32-76z m303-357q0-22-15-38t-38-16t-38 16t-16 38t16 38t38 16t38-16t15-38z m-116 250q0-18-13-32t-32-13t-31 13t-13 32t13 31t31 14t32-14t13-31z" horiz-adv-x="875" />
<glyph glyph-name="github-circled" unicode="&#xe800;" d="m857 350q0-140-82-252t-211-155q-15-3-22 4t-7 17v118q0 54-29 79q32 3 57 10t53 22t45 37t30 58t11 84q0 68-44 115q21 51-5 114q-15 5-45-6t-51-25l-21-13q-52 15-107 15t-108-15q-8 6-23 15t-47 22t-48 7q-24-63-4-114q-44-47-44-115q0-47 12-83t29-59t45-37t52-22t57-10q-22-20-27-58q-12-5-25-8t-32-3t-36 12t-31 35q-11 18-27 29t-28 14l-11 1q-12 0-16-2t-3-7t5-8t7-6l4-3q12-6 24-21t18-29l5-13q8-21 25-34t37-17t39-4t31 2l13 3q0-22 0-50t1-30q0-10-8-17t-22-4q-129 43-211 155t-82 252q0 117 58 215t155 156t216 58t215-58t156-156t57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="touiteur" unicode="&#xe801;" d="m904 622q-37-54-90-93q0-8 0-23q0-73-21-145t-64-139t-103-117t-144-82t-181-30q-151 0-276 81q19-3 43-3q126 0 224 77q-59 2-105 36t-64 89q19-2 34-2q24 0 48 6q-63 13-104 62t-41 115v2q38-21 82-23q-37 25-59 64t-22 86q0 49 25 91q68-83 164-133t208-55q-5 21-5 41q0 75 53 127t127 53q79 0 132-57q61 12 114 44q-20-64-79-100q52 6 104 28z" horiz-adv-x="928.6" />
<glyph glyph-name="flattr" unicode="&#xe803;" d="m180 424l0-210l-180-180l0 414q0 302 278 302l442 0l-312-310q-6-6-10-6q-8 0-12 10l0 128q-98 0-112-2q-94-16-94-146z m440 64l180 180l0-414q0-304-278-304l-440 0l312 312q2 6 8 6q8 0 12-10l0-128q98 0 112 2q94 16 94 146l0 210z" horiz-adv-x="800" />

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

BIN
public/img/LUTIm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
public/img/LUTIm.xcf Normal file

Binary file not shown.

BIN
public/img/LUTIm_flou.xcf Normal file

Binary file not shown.

BIN
public/img/LUTIm_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

114
public/js/bootstrap.js vendored
View File

@@ -1,5 +1,5 @@
/* ========================================================================
* Bootstrap: alert.js v3.1.0
* Bootstrap: alert.js v3.1.1
* http://getbootstrap.com/javascript/#alerts
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
@@ -88,115 +88,7 @@
}(jQuery);
/* ========================================================================
* Bootstrap: button.js v3.1.0
* http://getbootstrap.com/javascript/#buttons
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// BUTTON PUBLIC CLASS DEFINITION
// ==============================
var Button = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Button.DEFAULTS, options)
this.isLoading = false
}
Button.DEFAULTS = {
loadingText: 'loading...'
}
Button.prototype.setState = function (state) {
var d = 'disabled'
var $el = this.$element
var val = $el.is('input') ? 'val' : 'html'
var data = $el.data()
state = state + 'Text'
if (!data.resetText) $el.data('resetText', $el[val]())
$el[val](data[state] || this.options[state])
// push to event loop to allow forms to submit
setTimeout($.proxy(function () {
if (state == 'loadingText') {
this.isLoading = true
$el.addClass(d).attr(d, d)
} else if (this.isLoading) {
this.isLoading = false
$el.removeClass(d).removeAttr(d)
}
}, this), 0)
}
Button.prototype.toggle = function () {
var changed = true
var $parent = this.$element.closest('[data-toggle="buttons"]')
if ($parent.length) {
var $input = this.$element.find('input')
if ($input.prop('type') == 'radio') {
if ($input.prop('checked') && this.$element.hasClass('active')) changed = false
else $parent.find('.active').removeClass('active')
}
if (changed) $input.prop('checked', !this.$element.hasClass('active')).trigger('change')
}
if (changed) this.$element.toggleClass('active')
}
// BUTTON PLUGIN DEFINITION
// ========================
var old = $.fn.button
$.fn.button = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.button')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.button', (data = new Button(this, options)))
if (option == 'toggle') data.toggle()
else if (option) data.setState(option)
})
}
$.fn.button.Constructor = Button
// BUTTON NO CONFLICT
// ==================
$.fn.button.noConflict = function () {
$.fn.button = old
return this
}
// BUTTON DATA-API
// ===============
$(document).on('click.bs.button.data-api', '[data-toggle^=button]', function (e) {
var $btn = $(e.target)
if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
$btn.button('toggle')
e.preventDefault()
})
}(jQuery);
/* ========================================================================
* Bootstrap: modal.js v3.1.0
* Bootstrap: modal.js v3.1.1
* http://getbootstrap.com/javascript/#modals
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
@@ -440,7 +332,7 @@
}(jQuery);
/* ========================================================================
* Bootstrap: transition.js v3.1.0
* Bootstrap: transition.js v3.1.1
* http://getbootstrap.com/javascript/#transitions
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.

View File

@@ -200,7 +200,8 @@
//});
fd.append('format', 'json');
fd.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
fd.append('delete-day', ($("#delete-day").prop('checked')) ? 1 : 0);
fd.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
fd.append('delete-day', ($("#delete-day").val()));
widget.settings.onBeforeUpload.call(widget.element, widget.queuePos);

View File

@@ -10,5 +10,6 @@
// c.each(h.settings.extraData,function(i,j){f.append(i,j)});
f.append('format', 'json');
f.append('first-view', ($("#first-view").prop('checked')) ? 1 : 0);
f.append('delete-day', ($("#delete-day").prop('checked')) ? 1 : 0);
f.append('delete-day', ($("#delete-day").val()));
f.append('crypt', ($("#crypt").prop('checked')) ? 1 : 0);
h.settings.onBeforeUpload.call(h.element,h.queuePos);h.queueRunning=true;c.ajax({url:h.settings.url,type:h.settings.method,dataType:h.settings.dataType,data:f,cache:false,contentType:false,processData:false,forceSync:false,xhr:function(){var i=c.ajaxSettings.xhr();if(i.upload){i.upload.addEventListener("progress",function(m){var l=0;var j=m.loaded||m.position;var k=m.total||e.totalSize;if(m.lengthComputable){l=Math.ceil(j/k*100)}h.settings.onUploadProgress.call(h.element,h.queuePos,l)},false)}return i},success:function(j,i,k){h.settings.onUploadSuccess.call(h.element,h.queuePos,j)},error:function(k,i,j){h.settings.onUploadError.call(h.element,h.queuePos,j)},complete:function(i,j){h.processQueue()}})};c.fn.dmUploader=function(f){return this.each(function(){if(!c.data(this,b)){c.data(this,b,new a(this,f))}})};c(document).on("dragenter",function(f){f.stopPropagation();f.preventDefault()});c(document).on("dragover",function(f){f.stopPropagation();f.preventDefault()});c(document).on("drop",function(f){f.stopPropagation();f.preventDefault()})})(jQuery);

View File

@@ -0,0 +1,346 @@
function SimpleGraph(data, labels, canvas, settings) {
this.settings = settings;
setStyleDefaults(settings);
this.dataSet = new DataSet(data, labels, this.settings);
this.grid = new Grid(this.dataSet, this.settings);
this.canvas = canvas;
this.draw = function() {
if (this.settings.drawGrid) {
this.grid.draw(this.canvas);
}
if (this.settings.yAxisCaption) {
this.dataSet.labelYAxis(this.grid, this.canvas);
}
this.dataSet.labelXAxis(this.grid, this.canvas);
this.dataSet.plot(this.grid, this.canvas);
};
this.replaceDataSet = function(dataSet) {
this.dataSet = new DataSet(dataSet, dataSet.labels, this.settings);
this.grid = new Grid(this.dataSet, this.settings);
};
this.plotCurrentDataSet = function() {
this.dataSet.plot(this.grid, this.canvas);
};
function setStyleDefaults(settings) {
var targets = ["xAxisLabel", "yAxisLabel", "yAxisCaption", "hoverLabel", "hoverValue"];
var types = ["Color", "Font", "FontSize", "FontWeight"];
jQuery.each(targets, function(index, target) {
jQuery.each(types, function(index, type) {
if (!settings[target + type]) {
settings[target + type] = settings["label" + type];
}
});
});
settings.labelStyle = {
font: settings.labelFontSize + '"' + settings.labelFont + '"',
fill: settings.labelColor
};
jQuery.each(targets, function(index, target) {
settings[target + "Style"] = {
font: settings[target + "FontSize"] + ' ' + settings[target + "Font"],
fill: settings[target + "Color"],
"font-weight": settings[target + "FontWeight"]
};
});
}
}
// Holds the data and labels to be plotted, provides methods for labelling the x and y axes,
// and for plotting it's own points. Each method requires a grid object for translating values to
// x,y pixel coordinates and a canvas object on which to draw.
function DataSet(data, labels, settings) {
this.data = data;
this.labels = labels;
this.settings = settings;
this.labelXAxis = function(grid, canvas) {
(function(ds) {
jQuery.each(ds.labels, function(i, label) {
var x = grid.x(i);
canvas.text(x + ds.settings.xAxisLabelOffset, ds.settings.height - 6, label).attr(ds.settings.xAxisLabelStyle);
});
})(this);
};
this.labelYAxis = function(grid, canvas) {
// Legend
canvas.rect(
grid.leftEdge - (30 + this.settings.yAxisOffset), //TODO PARAM - Label Colum Width
grid.topEdge,
30, //TODO PARAM - Label Column Width
grid.height
).attr({stroke: this.settings.lineColor, fill: this.settings.lineColor, opacity: 0.3}); //TODO PARAMS - legend border and fill style
for (var i = 1, ii = (grid.rows); i < (ii - this.settings.lowerBound/2); i = i + 2) {
var value = (ii - i)*2,
y = grid.y(value) + 4, // TODO: Value of 4 works for default dimensions, expect will need to scale
x = grid.leftEdge - (6 + this.settings.yAxisOffset);
canvas.text(x, y, value).attr(this.settings.yAxisLabelStyle);
}
var caption = canvas.text(
grid.leftEdge - (20 + this.settings.yAxisOffset),
(grid.height/2) + (this.settings.yAxisCaption.length / 2),
this.settings.yAxisCaption + " (" + this.settings.units + ")").attr(this.settings.yAxisCaptionStyle).rotate(270);
// Increase the offset for the next caption (if any)
this.settings.yAxisOffset = this.settings.yAxisOffset + 30;
};
this.plot = function(grid, canvas) {
var line_path = canvas.path({
stroke: this.settings.lineColor,
"stroke-width": this.settings.lineWidth,
"stroke-linejoin": this.settings.lineJoin
});
var fill_path = canvas.path({
stroke: "none",
fill: this.settings.fillColor,
opacity: this.settings.fillOpacity
}).moveTo(this.settings.leftGutter, this.settings.height - this.settings.bottomGutter);
var bars = canvas.group(),
dots = canvas.group(),
cover = canvas.group();
var hoverFrame = dots.rect(10, 10, 100, 40, 5).attr({
fill: "#fff", stroke: "#474747", "stroke-width": 2}).hide(); //TODO PARAM - fill colour, border colour, border width
var hoverText = [];
hoverText[0] = canvas.text(60, 25, "").attr(this.settings.hoverValueStyle).hide();
hoverText[1] = canvas.text(60, 40, "").attr(this.settings.hoverLabelStyle).hide();
// Plot the points
(function(dataSet) {
jQuery.each(dataSet.data, function(i, value) {
var y = grid.y(value),
x = grid.x(i),
label = dataSet.labels ? dataSet.labels[i] : " ";
if (dataSet.settings.drawPoints) {
var dot = dots.circle(x, y, dataSet.settings.pointRadius).attr({fill: dataSet.settings.pointColor, stroke: dataSet.settings.pointColor});
}
if (dataSet.settings.drawBars) {
bars.rect(x + dataSet.settings.barOffset, y, dataSet.settings.barWidth, (dataSet.settings.height - dataSet.settings.bottomGutter) - y).attr({fill: dataSet.settings.barColor, stroke: "none"});
}
if (dataSet.settings.drawLine) {
line_path[i == 0 ? "moveTo" : "cplineTo"](x, y, 5);
}
if (dataSet.settings.fillUnderLine) {
fill_path[i == 0 ? "lineTo" : "cplineTo"](x, y, 5);
}
if (dataSet.settings.addHover) {
var rect = canvas.rect(x - 50, y - 50, 100, 100).attr({stroke: "none", fill: "#fff", opacity: 0}); //TODO PARAM - hover target width / height
jQuery(rect[0]).hover( function() {
jQuery.fn.simplegraph.hoverIn(canvas, value, label, x, y, hoverFrame, hoverText, dot, dataSet.settings);
},
function() {
jQuery.fn.simplegraph.hoverOut(canvas, hoverFrame, hoverText, dot, dataSet.settings);
});
}
});
})(this);
if (this.settings.fillUnderLine) {
fill_path.lineTo(grid.x(this.data.length - 1), this.settings.height - this.settings.bottomGutter).andClose();
}
hoverFrame.toFront();
};
}
// Holds the dimensions of the grid, and provides methods to convert values into x,y
// pixel coordinates. Also, provides a method to draw a grid on a supplied canvas.
function Grid(dataSet, settings) {
this.dataSet = dataSet;
this.settings = settings;
this.calculateMaxYAxis = function() {
var max = Math.max.apply(Math, this.dataSet.data),
maxOveride = this.settings.minYAxisValue;
if (maxOveride && maxOveride > max) {
max = maxOveride;
}
return max;
};
this.setYAxis = function() {
this.height = this.settings.height - this.settings.topGutter - this.settings.bottomGutter;
this.maxValueYAxis = this.calculateMaxYAxis();
this.Y = this.height / (this.maxValueYAxis - this.settings.lowerBound);
};
this.setXAxis = function() {
this.X = (this.settings.width - this.settings.leftGutter) / (this.dataSet.data.length - 0.4);
};
this.setDimensions = function() {
this.leftEdge = this.settings.leftGutter;
this.topEdge = this.settings.topGutter;
this.width = this.settings.width - this.settings.leftGutter - this.X;
this.columns = this.dataSet.data.length - 1;
this.rows = (this.maxValueYAxis - this.settings.lowerBound) / 2; //TODO PARAM - steps per row
};
this.draw = function(canvas) {
canvas.drawGrid(
this.leftEdge,
this.topEdge,
this.width,
this.height,
this.columns,
this.rows,
this.settings.gridBorderColor
);
};
this.x = function(value) {
return this.settings.leftGutter + this.X * value;
};
this.y = function(value) {
return this.settings.height - this.settings.bottomGutter - this.Y * (value - this.settings.lowerBound);
};
this.setYAxis();
this.setXAxis();
this.setDimensions();
};
(function($) {
//- required to implement hover function
var isLabelVisible;
var leaveTimer;
$.fn.simplegraph = function(data, labels, options) {
var settings = $.extend({}, $.fn.simplegraph.defaults, options);
setPenColor(settings);
return this.each( function() {
var canvas = Raphael(this, settings.width, settings.height);
var simplegraph = new SimpleGraph(data, labels, canvas, settings);
simplegraph.draw();
// Stash simplegraph object away for future reference
$.data(this, "simplegraph", simplegraph);
});
};
// Plot another set of values on an existing graph, use it like this:
// $("#target").simplegraph(data, labels).simplegraph_more(moreData);
$.fn.simplegraph_more = function(data, options) {
return this.each( function() {
var sg = $.data(this, "simplegraph");
sg.dataSet = new DataSet(data, sg.dataSet.labels, sg.settings);
sg.settings.penColor = options.penColor;
setPenColor(sg.settings);
sg.settings = $.extend(sg.settings, options);
sg.grid = new Grid(sg.dataSet, sg.settings);
sg.dataSet.labelYAxis(sg.grid, sg.canvas);
sg.dataSet.plot(sg.grid, sg.canvas);
});
};
// Public
$.fn.simplegraph.defaults = {
drawGrid: false,
units: "",
// Dimensions
width: 600,
height: 250,
leftGutter: 30,
bottomGutter: 20,
topGutter: 20,
// Label Style
labelColor: "#000",
labelFont: "Helvetica",
labelFontSize: "10px",
labelFontWeight: "normal",
// Grid Style
gridBorderColor: "#ccc",
// -- Y Axis Captions
yAxisOffset: 0,
// -- Y Axis Captions
xAxisLabelOffset: 0,
// Graph Style
// -- Points
drawPoints: false,
pointColor: "#000",
pointRadius: 3,
activePointRadius: 5,
// -- Line
drawLine: true,
lineColor: "#000",
lineWidth: 3,
lineJoin: "round",
// -- Bars
drawBars: false,
barColor: "#000",
barWidth: 10,
barOffset: 0,
// -- Fill
fillUnderLine: false,
fillColor: "#000",
fillOpacity: 0.2,
// -- Hover
addHover: true,
// Calculations
lowerBound: 0
};
// Default hoverIn callback, this is public and as such can be overwritten. You can write your
// own call back with the same signature if you want different behaviour.
$.fn.simplegraph.hoverIn = function(canvas, value, label, x, y, frame, hoverLabel, dot, settings) {
clearTimeout(leaveTimer);
var newcoord = {x: x * 1 + 7.5, y: y - 19};
if (newcoord.x + 100 > settings.width) {
newcoord.x -= 114;
}
hoverLabel[0].attr({text: value}).show().animate({x : newcoord.x + 50, y : newcoord.y + 15}, (isLabelVisible ? 100 : 0));
hoverLabel[1].attr({text: label}).show().animate({x : newcoord.x + 50, y : newcoord.y + 30}, (isLabelVisible ? 100 : 0));
frame.show().animate({x: newcoord.x, y: newcoord.y}, (isLabelVisible ? 100 : 0));
if (settings.drawPoints) {
dot.attr("r", settings.activePointRadius);
}
isLabelVisible = true;
canvas.safari();
};
// Default hoverOut callback, this is public and as such can be overwritten. You can write your
// own call back with the same signature if you want different behaviour.
$.fn.simplegraph.hoverOut = function(canvas, frame, label, dot, settings) {
if (settings.drawPoints) {
dot.attr("r", settings.pointRadius);
}
canvas.safari();
leaveTimer = setTimeout(function () {
isLabelVisible = false;
frame.hide();
label[0].hide();
label[1].hide();
canvas.safari();
}, 1);
};
// Private
function setPenColor(settings) {
if (settings.penColor) {
settings.lineColor = settings.penColor;
settings.pointColor = settings.penColor;
settings.fillColor = settings.penColor;
settings.barColor = settings.penColor;
}
}
})(jQuery);

2215
public/js/raphael.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
Raphael.el.isAbsolute = true;
Raphael.el.absolutely = function () {
this.isAbsolute = 1;
return this;
};
Raphael.el.relatively = function () {
this.isAbsolute = 0;
return this;
};
Raphael.el.moveTo = function (x, y) {
this._last = {x: x, y: y};
return this.attr({path: this.attrs.path + ["m", "M"][+this.isAbsolute] + parseFloat(x) + " " + parseFloat(y)});
};
Raphael.el.lineTo = function (x, y) {
this._last = {x: x, y: y};
return this.attr({path: this.attrs.path + ["l", "L"][+this.isAbsolute] + parseFloat(x) + " " + parseFloat(y)});
};
Raphael.el.arcTo = function (rx, ry, large_arc_flag, sweep_flag, x, y, angle) {
this._last = {x: x, y: y};
return this.attr({path: this.attrs.path + ["a", "A"][+this.isAbsolute] + [parseFloat(rx), parseFloat(ry), +angle, large_arc_flag, sweep_flag, parseFloat(x), parseFloat(y)].join(" ")});
};
Raphael.el.curveTo = function () {
var args = Array.prototype.splice.call(arguments, 0, arguments.length),
d = [0, 0, 0, 0, "s", 0, "c"][args.length] || "";
this.isAbsolute && (d = d.toUpperCase());
this._last = {x: args[args.length - 2], y: args[args.length - 1]};
return this.attr({path: this.attrs.path + d + args});
};
Raphael.el.cplineTo = function (x, y, w) {
this.attr({path: this.attrs.path + ["C", this._last.x + w, this._last.y, x - w, y, x, y]});
this._last = {x: x, y: y};
return this;
};
Raphael.el.qcurveTo = function () {
var d = [0, 1, "t", 3, "q"][arguments.length],
args = Array.prototype.splice.call(arguments, 0, arguments.length);
if (this.isAbsolute) {
d = d.toUpperCase();
}
this._last = {x: args[args.length - 2], y: args[args.length - 1]};
return this.attr({path: this.attrs.path + d + args});
};
Raphael.el.addRoundedCorner = function (r, dir) {
var rollback = this.isAbsolute;
rollback && this.relatively();
this._last = {x: r * (!!(dir.indexOf("r") + 1) * 2 - 1), y: r * (!!(dir.indexOf("d") + 1) * 2 - 1)};
this.arcTo(r, r, 0, {"lu": 1, "rd": 1, "ur": 1, "dl": 1}[dir] || 0, this._last.x, this._last.y);
rollback && this.absolutely();
return this;
};
Raphael.el.andClose = function () {
return this.attr({path: this.attrs.path + "z"});
};

6
templates/about.html.ep Normal file
View File

@@ -0,0 +1,6 @@
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
% my $scheme = (defined(config('https')) && config('https')) ? 'https' : 'http';
<div class="modal-body">
<%==l 'informations-body', url_for('/')->base->scheme($scheme)->to_abs().'/', config('contact') %>
<%= link_to url_for('index') => ( class => "btn btn-primary btn-lg" ) => begin %><%=l 'back-to-index' %><% end%>
</div>

View File

@@ -0,0 +1,14 @@
<div id="stats-data">
<table class="table table-striped table-bordered">
<thead>
<tr>
</tr>
</thead>
<tbody>
<tr>
</tr>
<tr>
</tr>
</tbody>
</table>
</div>

View File

@@ -1,19 +1,26 @@
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
% my $scheme = (defined(config('https')) && config('https')) ? 'https' : 'http';
<div class="messages">
% if (defined(flash('short'))) {
% if (config('always_encrypt')) {
<p><%=l 'always_encrypt' %></p>
% }
% if (defined(stash('short'))) {
<div class="alert alert-success">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<strong><%= flash('filename') %></strong>
<ul>
<li><%=l 'view-link'%><%= link_to url_for('/')->to_abs.flash('short') => begin %><%= url_for('/')->to_abs.flash('short') %><%= end %></li>
<li><%=l 'download-link' %> <%= link_to url_for('/')->to_abs.flash('short').'?dl' => begin %><%= url_for('/')->to_abs.flash('short').'?dl' %><%= end %></li>
<li><%=l 'twitter-link' %> <%= link_to url_for('/')->to_abs.flash('short').'?t' => begin %><%= url_for('/')->to_abs.flash('short').'?t' %><%= end %></li>
</ul>
% if (defined(stash('short'))) {
<img class="pull-left thumbnail" alt="<%= stash('filename') %> thumbnail" src="<%= stash('thumb') %>">
% }
<div>
<strong><%= stash('filename') %></strong>
<ul class="list-unstyled">
<li><i class="icon icon-eye" title="<%=l 'view-link' %>"></i> <%= link_to url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short') => begin %><%= url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short') %><%= end %></li>
<li><i class="icon icon-download" title="<%=l 'download-link' %>"></i> <%= link_to url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short').'?dl' => begin %><%= url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short').'?dl' %><%= end %></li>
<li><i class="icon icon-touiteur" title="<%=l 'twitter-link' %>"></i> <%= link_to url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short').'?t' => begin %><%= url_for('/')->base->scheme($scheme)->to_abs.'/'.stash('short').'?t' %><%= end %></li>
</ul>
</div>
</div>
% }
% if (defined(flash('msg'))) {
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<strong><%=l 'some-bad' %></strong><br>
<%= flash('filename') %> <%= flash('msg') %>
</div>
@@ -21,22 +28,45 @@
</div>
<noscript>
<form class="form" role="form" method="POST" action="<%== url_for 'add' %>" enctype="multipart/form-data">
<form class="form" role="form" method="POST" action="<%== url_for('add') %>" enctype="multipart/form-data">
<div class="form-group form-inline">
<select name="delete-day" class="form-control">
% for my $delay (qw/0 1 7 30 365/) {
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
% if (config('max_delay')) {
% if ($delay) {
% if ($delay < config('max_delay')) {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% } elsif ($delay == config('max_delay')) {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% last;
% } else {
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>
% last;
% }
% }
% } else {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% }
% }
</select>
<div class="checkbox">
<label>
<input type="checkbox" name="first-view"> <%=l 'delete-first' %>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="delete-day"> <%=l 'delete-day' %>
<label class="always-encrypt">
<input type="checkbox" name="crypt"> <%=l 'crypt_image' %>
</label>
</div>
</div>
<div class="form-group">
<label for="lutim-file"><%=l 'upload_image' %></label>
<input type="file" name="file" id="lutim-file" required></input>
<input type="file" name="file" id="lutim-file">
</div>
<div class="form-group">
<label for="lutim-file-url"><%=l 'upload_image_url' %></label>
<input type="url" name="lutim-file-url" placeholder="<%=l 'image_url' %>">
<p class="help-block"><%=l 'image-only' %></p>
</div>
<%= submit_button l('go'), class => 'btn btn-default btn-primary', id => 'submitbutton' %>
@@ -46,14 +76,33 @@
<!-- D&D Zone-->
<div class="jsonly">
<div class="form-group form-inline">
<select id="delete-day" class="form-control">
% for my $delay (qw/0 1 7 30 365/) {
% my $text = ($delay == 7 || $delay == 30) ? l('delay_days', $delay) : l("delay_$delay");
% if (config('max_delay')) {
% if ($delay) {
% if ($delay < config('max_delay')) {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% } elsif ($delay == config('max_delay')) {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% last;
% } else {
% my $text = ($delay == 1) ? l('delay_1') : l('delay_days', $delay);
<option value="<%= config('max_delay') %>" <%== is_selected(config('max_delay')) %>><%=l('delay_days', config('max_delay')) %></option>
% last;
% }
% }
% } else {
<option value="<%= $delay %>" <%== is_selected($delay) %>><%= $text %></option>
% }
% }
</select>
<div class="checkbox">
<label>
<input type="checkbox" id="first-view"> <%=l 'delete-first' %>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="delete-day"> <%=l 'delete-day' %>
<label class="always-encrypt">
<input type="checkbox" id="crypt"> <%=l 'crypt_image' %>
</label>
</div>
</div>
@@ -63,35 +112,50 @@
<div class="browser">
<label>
<span><%=l 'file-browser' %></span>
<input type="file" name="files[]" multiple="multiple" title='Click to add Files'>
<input type="file" name="files[]" multiple="multiple" title='<%=l 'file-browser' %>'>
</label>
</div>
</div>
<p class="help-block"><%=l 'image-only' %></p>
<form class="form-horizontal" role="form" method="POST" action="<%== url_for('add') %>">
<div class="form-group">
<span class="col-sm-3"><span class="hidden-spin" style="font-size:200%; display:none;" > <i class="icon-spinner animate-spin pull-right"></i></span><label for="lutim-file-url" class="control-label pull-right"><%=l 'upload_image_url' %></label></span>
<div class="col-sm-9">
<input type="url" name="file-url" class="form-control" id="lutim-file-url" placeholder="<%=l 'image_url' %>">
</div>
</div>
<a href="#" class="btn btn-default btn-primary pull-right" id="file-url-button"><%=l 'go' %></a>
</form>
</div>
<!-- /D&D Zone -->
%= javascript 'js/dmuploader.min.js'
%= javascript begin
function link(url, dl) {
if (dl !== '') {
url = url+'?'+dl;
}
return '<a href="<%== url_for('index')->to_abs() %>'+url+'"><%== url_for('index')->to_abs() %>'+url+'</a>';
return '<a href="<%== url_for('index')->base->scheme($scheme)->to_abs() %>/'+url+'"><%== url_for('index')->base->scheme($scheme)->to_abs() %>/'+url+'</a>';
}
function message(success, msg) {
if(success) {
return '<div class="alert alert-success"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button><strong>'
var thumb = (msg.thumb !== null) ? '<img class="pull-left thumbnail" alt="'+msg.filename+' thumbnail" src="'+msg.thumb+'">' : ''
return '<div class="alert alert-success"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>'
+thumb
+'<div><strong>'
+msg.filename
+'</strong><ul><li><%=l 'view-link'%>'
+'</strong>'
+'<ul class="list-unstyled"><li><i class="icon icon-eye" title="<%=l 'view-link' %>"></i>&nbsp;'
+link(msg.short, '')
+'</a></li><li><%=l 'download-link' %>'
+'</a></li><li><i class="icon icon-download" title="<%=l 'download-link' %>"></i>&nbsp;'
+link(msg.short, 'dl')
+'</a></li><li><%=l 'twitter-link' %>'
+'</a></li><li><i class="icon icon-touiteur" title="<%=l 'twitter-link' %>"></i>&nbsp;'
+link(msg.short, 't')
+'</li></div>';
+'</li></ul></div>';
} else {
return '<div class="alert alert-danger"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button><strong><%=l 'some-bad' %></strong><br>'
+msg.filename
+' '
+"<br>"
+msg.msg
+'</div>';
}
@@ -101,21 +165,71 @@
url: '<%== url_for('add') %>',
dataType: 'json',
allowedTypes: 'image/*',
maxFileSize: <%= $max_file_size %>,
onNewFile: function(id, file){
$(".messages").append('<div id="'+id+'-div">'+file.name+'<br><div class="progress"><div id="'+id+'"class="progress-bar progress-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"><span id="'+id+'-text" class="pull-left" style="padding-left: 10px;"> 0%</span></div></div></div>');
},
onUploadProgress: function(id, percent){
var percentStr = ' '+percent+'%';
$('#'+id).prop('aria-valuenow', percent);
$('#'+id).prop('style', 'width: '+percent+'%;');
$('#'+id+'-text').html(percentStr);
},
onUploadSuccess: function(id, data){
$('#'+id+'-div').remove();
$(".messages").append(message(data.success, data.msg));
},
onUploadError: function(id, message){
$(".messages").append(message(false, ''));
},
onFileSizeError: function(file){
$(".messages").append(message(false, { filename: file.name, msg: '<%= l('file_too_big', $max_file_size) %>'}));
}
});
};
}
function upload_url() {
var val = $("#lutim-file-url").val();
if (val !== undefined && val !== "") {
$("#lutim-file-url").prop('disabled', 'disabled');
$(".hidden-spin").css('display', 'block');
console.log(val);
$.ajax({
url : '<%== url_for('add') %>',
type : "POST",
data : {
'lutim-file-url' : val,
'format' : 'json',
'first-view' : ($("#first-view").prop('checked')) ? 1 : 0,
'crypt' : ($("#crypt").prop('checked')) ? 1 : 0,
'delete-day' : $("#delete-day").val()
},
success: function(data) {
$(".messages").append(message(data.success, data.msg));
if (data.success) {
$("#lutim-file-url").val('');
}
},
error: function() {
$(".messages").append(message(false, ''));
},
complete: function() {
$("#lutim-file-url").prop('disabled', '');
$(".hidden-spin").css('display', 'none');
}
});
} else {
console.log("fhdsjnf");
}
}
$('document').ready(function() {
$('.jsonly').show();
var firstview = ($("#first-view").prop('checked')) ? 1 : 0;
var deleteday = ($("#delete-day").prop('checked')) ? 1 : 0;
bindddz(firstview, deleteday);
$("#file-url-button").on("click", upload_url);
});
% end

View File

@@ -1,7 +1,8 @@
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
% use Mojo::Util qw(url_escape);
% my $twitter_url = 'https://twitter.com/share';
% my $url = url_for('/')->to_abs();
% my $scheme = (defined(config('https')) && config('https')) ? 'https' : 'http';
% my $url = url_for('/')->base->scheme($scheme)->to_abs().'/';
% $twitter_url .= '?url='.url_escape("$url")
% .'&via=framasky'
% .'&text=Check out this %23LUTIm instance! ';
@@ -11,14 +12,15 @@
<title>LUTIm</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="<%= url_for('/') %>img/favicon.png">
%= stylesheet 'css/bootstrap.min.css', media => 'screen'
%= stylesheet 'css/fontello.css'
%= stylesheet 'css/animation.css'
%= stylesheet 'css/uploader.css'
%= stylesheet begin
body {
padding-top: 40px;
padding-bottom: 40px;
background-color: #eee;
}
.container {
@@ -29,13 +31,27 @@
.jsonly {
display: none;
}
.thumbnail {
margin-right: 8px;
}
% if (config('always_encrypt')) {
label.always-encrypt {
display: none;
}
% }
% end
%= javascript 'js/jquery-2.1.0.min.js'
%= javascript 'js/bootstrap.min.js'
%= javascript 'js/dmuploader.min.js'
%= javascript begin
$('document').ready(function() {
$('.jsonly').show();
});
% end
</head>
<body>
<div class="container">
<div class="container-fluid">
<div>
% if (defined(config('hosted_by'))) {
<div class="pull-right">
@@ -43,11 +59,15 @@
</div>
% }
<div>
<div class="pull-left hidden-xs">
<img src="<%= url_for('/') %>img/LUTIm_small.png" alt="LUTIm logo">
</div>
<h1>Let's Upload That Image!</h1>
<p>
&copy; 2014 <%= link_to 'http://www.fiat-tux.fr' => begin %>Luc Didry<% end %> — 
<%=l 'license' %> <%= link_to 'https://www.gnu.org/licenses/agpl-3.0.html' => begin %>AGPL<% end %> — 
<span class="jsonly"><a data-toggle="modal" href="#myModal"><%=l 'informations' %></a> — </span>
<noscript><%= link_to url_for('about') => begin %><%=l 'informations' %><% end %> — </noscript>
<%= link_to 'https://github.com/ldidry/lutim' => (title => l 'fork-me') => begin %><i class="lead icon icon-github-circled"></i><% end %> 
<%= link_to $twitter_url => (title => l 'share-twitter') => begin %><i class="lead icon icon-touiteur"></i><% end %> 
<%= link_to 'https://flattr.com/submit/auto?user_id=_SKy_&url='.$url.'&title=LUTIm&category=software' => (title => 'Flattr this') => begin %><i class="lead icon icon-flattr"></i><% end %> 
@@ -55,6 +75,16 @@
</p>
</div>
</div>
% if (defined(config('broadcast_message'))) {
<div class="alert alert-info">
<strong><%= config('broadcast_message') %></strong>
</div>
% }
% if (defined(stash('stop_upload'))) {
<div class="alert alert-danger">
<strong><%= stash('stop_upload') %></strong>
</div>
% }
<%= content %>
<div class="modal fade bs-modal-lg" id="myModal" role="dialog">
<div class="modal-dialog">
@@ -64,7 +94,7 @@
<h3 class="modal-title">LUTIm</h3>
</div>
<div class="modal-body">
<%==l 'informations-body', url_for('index')->to_abs(), config('contact') %>
<%==l 'informations-body', url_for('/')->base->scheme($scheme)->to_abs().'/', config('contact') %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@@ -72,14 +102,6 @@
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<noscript>
<div class="modal-header">
<h3 class="modal-title">LUTIm</h3>
</div>
<div class="modal-body">
<%==l 'informations-body', url_for('index')->to_abs(), config('contact') %>
</div>
</noscript>
</div>
% if (defined(config('piwik_img'))) {
<img src="<%== config('piwik_img') %>" style="border:0" alt="" />

64
templates/stats.html.ep Normal file
View File

@@ -0,0 +1,64 @@
% # vim:set sts=4 sw=4 ts=4 ft=html.epl expandtab:
%= javascript 'js/raphael.js'
%= javascript 'js/raphael.path.methods.js'
%= javascript 'js/jquery.simplegraph.js'
%= include 'data'
<div id="evol-holder"></div>
<div id="total-holder"></div>
<p><small><%=l 'graph-data-once-a-day' %></small></p>
<h4><%= $total %><%=l 'pushed-images' %></h4>
<%= link_to url_for('index') => ( class => "btn btn-primary btn-lg" ) => begin %><%=l 'back-to-index' %><% end%>
%= javascript begin
function graph(stats_data, stats_labels, stats_total) {
// Plot the data
// - Temperature Graph - adds colour, fill, and a minimum value for the y axis
$("#evol-holder").simplegraph(
stats_data,
stats_labels,
{
addHover: true,
penColor: "#f00",
fillUnderLine: true,
drawPoints: true,
width: document.body.offsetWidth - 50
}
)
$("#total-holder").simplegraph(
stats_total,
stats_labels,
{
addHover: true,
penColor: "#00f",
fillUnderLine: true,
drawPoints: true,
width: document.body.offsetWidth - 50
}
);
}
$(document).ready(function() {
// Get the data
var stats_labels = [], stats_data = [], stats_total = [];
$("#stats-data thead th").each(function () {
stats_labels.push($(this).html());
});
$("#stats-data tbody tr:first-child td").each(function () {
stats_data.push($(this).html());
});
$("#stats-data tbody tr:nth-child(2) td").each(function () {
stats_total.push($(this).html());
});
// Hide the data
$("#stats-data").hide();
graph(stats_data, stats_labels, stats_total);
$(window).resize(function() {
$("#evol-holder").empty();
$("#total-holder").empty();
graph(stats_data, stats_labels, stats_total);
});
});
% end

View File

@@ -1,18 +1,20 @@
% # vim:set sw=4 ts=4 sts=4 ft=html.epl expandtab:
% my $scheme = (defined(config('https')) && config('https')) ? 'https' : 'http';
<!DOCTYPE html>
<html>
<html style="height:100%;">
<head>
<title>LUTIm</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8" />
<link rel="icon" type="image/png" href="<%= url_for('/')->base->scheme($scheme) %>/img/favicon.png">
<meta name="twitter:card" content="photo">
% if (defined(config('tweet_card_via'))) {
<meta name="twitter:site" content="<%= config('tweet_card_via') %>">
% }
<meta name="twitter:image:src" content="<%= url_for('/')->to_abs().$short %>">
<meta name="twitter:image:src" content="<%= url_for('/')->base->scheme($scheme)->to_abs().'/'.$short %>">
</head>
<body>
<img src="<%= url_for('/').$short %>" alt="<%= $filename %>">
<body style="height: 97%;">
<img style="max-width:100%; max-height:100%;" src="<%= url_for('/').$short %>" alt="<%= $filename %>">
</body>
</html>