Database support #1
37 changed files with 692 additions and 243 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,2 @@
|
|||
*.db
|
||||
*.conf
|
||||
application.conf
|
||||
|
|
2
README
2
README
|
@ -1,2 +1,4 @@
|
|||
Small script for store images.
|
||||
Based on http://d.hatena.ne.jp/yukikimoto/20100212/1265989676 with small modifications.
|
||||
|
||||
URL format: /images/<user_id>/<size>/<image_name>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
password => '',
|
||||
db_file => 'sql/fotostore.db',
|
||||
invite_code => 'very_secure_invite_code',
|
||||
}
|
5
cpanfile
5
cpanfile
|
@ -5,4 +5,7 @@ requires 'File::Basename';
|
|||
requires 'File::Path';
|
||||
requires 'File::Spec';
|
||||
requires 'Cwd';
|
||||
requires 'Getopt::Long';
|
||||
requires 'Getopt::Long';
|
||||
requires 'DBI';
|
||||
requires 'DBD::SQLite';
|
||||
requires 'Digest::SHA';
|
397
fotostore.pl
Normal file → Executable file
397
fotostore.pl
Normal file → Executable file
|
@ -2,7 +2,8 @@
|
|||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Mojolicious::Lite; # app, get, post is exported.
|
||||
use lib 'lib';
|
||||
use Mojolicious::Lite; # app, get, post is exported.
|
||||
|
||||
use File::Basename 'basename';
|
||||
use File::Path 'mkpath';
|
||||
|
@ -10,65 +11,52 @@ use File::Spec 'catfile';
|
|||
use Cwd;
|
||||
|
||||
use Imager;
|
||||
use DBI;
|
||||
use Digest::SHA;
|
||||
|
||||
my $config = plugin 'Config'=> {file => 'application.conf'};;
|
||||
use FotoStore::DB;
|
||||
|
||||
my $predefined_user = 'alpha6';
|
||||
my $predefined_password = $config->{'password'};
|
||||
use Data::Dumper;
|
||||
$Data::Dumper::Maxdepth = 3;
|
||||
|
||||
die "No user password defined!" unless($predefined_password);
|
||||
my $config = plugin 'Config' => { file => 'application.conf' };
|
||||
|
||||
my $db = FotoStore::DB->new( $config->{'db_file'} );
|
||||
|
||||
# Image base URL
|
||||
my $IMAGE_BASE = 'images';
|
||||
my $ORIG_DIR = 'orig';
|
||||
my $ORIG_DIR = 'orig';
|
||||
|
||||
my $thumbs_size = 200;
|
||||
|
||||
my @scale_width = ($thumbs_size, 640, 800, 1024);
|
||||
my @scale_width = ( $thumbs_size, 640, 800, 1024 );
|
||||
|
||||
my $sha = Digest::SHA->new('sha256');
|
||||
|
||||
# Directory to save image files
|
||||
# (app is Mojolicious object. static is MojoX::Dispatcher::Static object)
|
||||
my $IMAGE_DIR = File::Spec->catfile(getcwd(), 'public', $IMAGE_BASE);
|
||||
|
||||
# Create directory if not exists
|
||||
unless (-d $IMAGE_DIR) {
|
||||
mkpath $IMAGE_DIR or die "Cannot create directory: $IMAGE_DIR";
|
||||
}
|
||||
|
||||
my $ORIG_PATH = File::Spec->catfile($IMAGE_DIR, $ORIG_DIR);
|
||||
unless (-d $ORIG_PATH) {
|
||||
mkpath $ORIG_PATH or die "Cannot create directory: $ORIG_PATH";
|
||||
}
|
||||
|
||||
for my $dir (@scale_width) {
|
||||
my $scaled_dir_path = File::Spec->catfile($IMAGE_DIR, $dir);
|
||||
unless (-d $scaled_dir_path) {
|
||||
mkpath $scaled_dir_path or die "Cannot create directory: $scaled_dir_path";
|
||||
}
|
||||
}
|
||||
my $IMAGE_DIR = File::Spec->catfile( getcwd(), 'public', $IMAGE_BASE );
|
||||
|
||||
plugin 'authentication', {
|
||||
autoload_user => 1,
|
||||
load_user => sub {
|
||||
load_user => sub {
|
||||
my $self = shift;
|
||||
my $uid = shift;
|
||||
|
||||
return {
|
||||
'username' => $predefined_user,
|
||||
'password' => $predefined_password,
|
||||
'name' => 'User Name'
|
||||
} if ($uid eq 'userid' || $uid eq 'useridwithextradata');
|
||||
return undef;
|
||||
|
||||
return $db->get_user($uid);
|
||||
},
|
||||
validate_user => sub {
|
||||
my $self = shift;
|
||||
my $username = shift || '';
|
||||
my $password = shift || '';
|
||||
my $self = shift;
|
||||
my $username = shift || '';
|
||||
my $password = shift || '';
|
||||
my $extradata = shift || {};
|
||||
|
||||
# return 'useridwithextradata' if($username eq 'alpha6' && $password eq 'qwerty' && ( $extradata->{'ohnoes'} || '' ) eq 'itsameme');
|
||||
return 'userid' if($username eq $predefined_user && $password eq $predefined_password);
|
||||
return undef;
|
||||
|
||||
my $digest = $sha->add($password);
|
||||
|
||||
my $user_id = $db->check_user( $username, $digest->hexdigest() );
|
||||
$self->app->log->debug("user id: [$user_id]");
|
||||
|
||||
return $user_id;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -76,56 +64,108 @@ post '/login' => sub {
|
|||
my $self = shift;
|
||||
my $u = $self->req->param('username');
|
||||
my $p = $self->req->param('password');
|
||||
|
||||
if ($self->authenticate($u, $p)) {
|
||||
|
||||
if ( $self->authenticate( $u, $p ) ) {
|
||||
$self->redirect_to('/');
|
||||
} else {
|
||||
$self->render(text => 'Login failed :(');
|
||||
}
|
||||
|
||||
else {
|
||||
$self->render( text => 'Login failed :(' );
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
get '/logout' => sub {
|
||||
my $self = shift;
|
||||
|
||||
|
||||
$self->logout();
|
||||
$self->render(text => 'bye');
|
||||
$self->render( text => 'bye' );
|
||||
};
|
||||
|
||||
get '/register' => ( authenticated => 0 ) => sub {
|
||||
|
||||
};
|
||||
|
||||
post '/register' => ( authenticated => 0 ) => sub {
|
||||
my $self = shift;
|
||||
my $username = $self->req->param('username');
|
||||
my $password = $self->req->param('password');
|
||||
my $fullname = $self->req->param('fullname');
|
||||
my $invite = $self->req->param('invite');
|
||||
|
||||
if ($invite eq $config->{'invite_code'}) {
|
||||
#chek that username is not taken
|
||||
my $user = $db->get_user($username);
|
||||
if ($user->{'user_id'} > 0) {
|
||||
$self->render(template => 'error', message => 'Username already taken!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($fullname eq '') {
|
||||
$fullname = $username;
|
||||
}
|
||||
|
||||
my $digest = $sha->add($password);
|
||||
$db->add_user($username, $digest->hexdigest(), $fullname);
|
||||
|
||||
#Authenticate user after add
|
||||
if ( $self->authenticate( $username, $password ) ) {
|
||||
$self->redirect_to('/');
|
||||
}
|
||||
else {
|
||||
$self->render( text => 'Login failed :(' );
|
||||
}
|
||||
|
||||
} else {
|
||||
$self->render(template => 'error', message => 'invalid invite code');
|
||||
}
|
||||
};
|
||||
|
||||
# Display top page
|
||||
get '/' => sub {
|
||||
my $self = shift;
|
||||
|
||||
my $current_user = $self->current_user;
|
||||
|
||||
my $files_list = $db->get_files($current_user->{'user_id'}, 20);
|
||||
|
||||
my $thumbs_dir = File::Spec->catfile($IMAGE_DIR, $thumbs_size);
|
||||
# Get file names(Only base name)
|
||||
my @images = map {basename($_)} glob("$thumbs_dir/*.jpg $thumbs_dir/*.gif $thumbs_dir/*.png");
|
||||
|
||||
# Sort by new order
|
||||
@images = sort {$b cmp $a} @images;
|
||||
my $thumbs_dir = File::Spec->catfile( $IMAGE_DIR, $current_user->{'user_id'}, $thumbs_size );
|
||||
|
||||
my @images = map { $_->{'file_name'} } @$files_list;
|
||||
|
||||
# Render
|
||||
return $self->render(images => \@images, image_base => $IMAGE_BASE, orig => $ORIG_DIR, thumbs_size => $thumbs_size, scales => \@scale_width);
|
||||
return $self->render(
|
||||
images => \@images,
|
||||
image_base => $IMAGE_BASE,
|
||||
orig => $ORIG_DIR,
|
||||
thumbs_size => $thumbs_size,
|
||||
scales => \@scale_width,
|
||||
user_id => $current_user->{'user_id'},
|
||||
);
|
||||
|
||||
} => 'index';
|
||||
|
||||
# Upload image file
|
||||
post '/upload' => (authenticated => 1)=> sub {
|
||||
post '/upload' => ( authenticated => 1 ) => sub {
|
||||
my $self = shift;
|
||||
|
||||
# Uploaded image(Mojo::Upload object)
|
||||
my $image = $self->req->upload('image');
|
||||
|
||||
|
||||
my $user = $self->current_user();
|
||||
my $user_id = $user->{'user_id'};
|
||||
$self->app->log->debug( "user:" . Dumper($user) );
|
||||
|
||||
# Not upload
|
||||
unless ($image) {
|
||||
return $self->render(
|
||||
template => 'error',
|
||||
template => 'error',
|
||||
message => "Upload fail. File is not specified."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
# Upload max size
|
||||
#my $upload_max_size = 3 * 1024 * 1024;
|
||||
|
||||
|
||||
# Over max size
|
||||
#if ($image->size > $upload_max_size) {
|
||||
# return $self->render(
|
||||
|
@ -133,210 +173,95 @@ post '/upload' => (authenticated => 1)=> sub {
|
|||
# message => "Upload fail. Image size is too large."
|
||||
# );
|
||||
#}
|
||||
|
||||
|
||||
# Check file type
|
||||
my $image_type = $image->headers->content_type;
|
||||
my %valid_types = map {$_ => 1} qw(image/gif image/jpeg image/png);
|
||||
|
||||
my %valid_types = map { $_ => 1 } qw(image/gif image/jpeg image/png);
|
||||
|
||||
# Content type is wrong
|
||||
unless ($valid_types{$image_type}) {
|
||||
unless ( $valid_types{$image_type} ) {
|
||||
return $self->render(
|
||||
template => 'error',
|
||||
message => "Upload fail. Content type is wrong."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
# Extention
|
||||
my $exts = {'image/gif' => 'gif', 'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png'};
|
||||
my $exts = {
|
||||
'image/gif' => 'gif',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png'
|
||||
};
|
||||
my $ext = $exts->{$image_type};
|
||||
|
||||
|
||||
# Image file
|
||||
my $filename = create_filename($ext);
|
||||
my $image_file = File::Spec->catfile($ORIG_PATH, $filename);
|
||||
|
||||
# If file is exists, Retry creating filename
|
||||
while(-f $image_file){
|
||||
$filename = create_filename();
|
||||
$image_file = File::Spec->catfile($ORIG_PATH, $filename);
|
||||
}
|
||||
|
||||
my $filename = sprintf( '%s.%s', create_hash( $image->slurp() ), $ext );
|
||||
my $image_file = File::Spec->catfile( get_path($user_id, $ORIG_DIR), $filename );
|
||||
|
||||
# Save to file
|
||||
$image->move_to($image_file);
|
||||
|
||||
|
||||
my $imager = Imager->new();
|
||||
$imager->read(file => $image_file) or die $imager->errstr;
|
||||
$imager->read( file => $image_file ) or die $imager->errstr;
|
||||
|
||||
#http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
#http://myjaphoo.de/docs/exifidentifiers.html
|
||||
my $rotation_angle = $imager->tags( name => "exif_orientation") || 1;
|
||||
$self->app->log->info("Rotation angle [".$rotation_angle."] [".$image->filename."]");
|
||||
my $rotation_angle = $imager->tags( name => "exif_orientation" ) || 1;
|
||||
$self->app->log->info(
|
||||
"Rotation angle [" . $rotation_angle . "] [" . $image->filename . "]" );
|
||||
|
||||
if ($rotation_angle == 3) {
|
||||
$imager = $imager->rotate(degrees=>180);
|
||||
}
|
||||
elsif ($rotation_angle == 6) {
|
||||
$imager = $imager->rotate(degrees=>90);
|
||||
}
|
||||
if ( $rotation_angle == 3 ) {
|
||||
$imager = $imager->rotate( degrees => 180 );
|
||||
}
|
||||
elsif ( $rotation_angle == 6 ) {
|
||||
$imager = $imager->rotate( degrees => 90 );
|
||||
}
|
||||
|
||||
for my $scale (@scale_width) {
|
||||
my $scaled = $imager->scale(xpixels => $scale);
|
||||
|
||||
$scaled->write(file => File::Spec->catfile($IMAGE_DIR, $scale, $filename)) or die $scaled->errstr;
|
||||
}
|
||||
|
||||
my $scaled = $imager->scale( xpixels => $scale );
|
||||
|
||||
$self->render(json => {files => [
|
||||
{
|
||||
name => $image->filename,
|
||||
size => $image->size,
|
||||
url => sprintf('/images/orig/%s', $filename),
|
||||
thumbnailUrl => sprintf('/images/200/%s', $filename),
|
||||
}]
|
||||
});
|
||||
$scaled->write(
|
||||
file => File::Spec->catfile( get_path($user_id, $scale), $filename ) )
|
||||
or die $scaled->errstr;
|
||||
}
|
||||
|
||||
if ( !$db->add_file( $user->{'user_id'}, $filename ) ) {
|
||||
|
||||
#TODO: Send error msg
|
||||
}
|
||||
|
||||
$self->render(
|
||||
json => {
|
||||
files => [
|
||||
{
|
||||
name => $image->filename,
|
||||
size => $image->size,
|
||||
url => sprintf( '/images/orig/%s', $filename ),
|
||||
thumbnailUrl => sprintf( '/images/200/%s', $filename ),
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
# Redirect to top page
|
||||
# $self->redirect_to('index');
|
||||
|
||||
|
||||
} => 'upload';
|
||||
|
||||
sub create_filename {
|
||||
my $ext = shift || 'jpg';
|
||||
|
||||
# Date and time
|
||||
my ($sec, $min, $hour, $mday, $month, $year) = localtime;
|
||||
$month = $month + 1;
|
||||
$year = $year + 1900;
|
||||
|
||||
# Random number(0 ~ 999999)
|
||||
my $rand_num = int(rand 1000000);
|
||||
sub create_hash {
|
||||
my $data_to_hash = shift;
|
||||
|
||||
# Create file name form datatime and random number
|
||||
# (like image-20091014051023-78973)
|
||||
my $name = sprintf('image-%04s%02s%02s%02s%02s%02s-%06s.%s',
|
||||
$year, $month, $mday, $hour, $min, $sec, $rand_num, $ext);
|
||||
|
||||
return $name;
|
||||
$sha->add($data_to_hash);
|
||||
return $sha->hexdigest();
|
||||
}
|
||||
|
||||
sub get_path {
|
||||
my ($user_id, $size) = @_;
|
||||
my $path = File::Spec->catfile( $IMAGE_DIR, $user_id, $size );
|
||||
unless (-d $path) {
|
||||
mkpath $path or die "Cannot create directory: $path";
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
app->start;
|
||||
|
||||
__DATA__
|
||||
|
||||
@@ error.html.ep
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<%= $message %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ no_logged.html.ep
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<div>
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ index.html.ep
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Bootstrap styles -->
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
|
||||
<!-- Generic page styles -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/style.css">
|
||||
<!-- blueimp Gallery styles -->
|
||||
<link rel="stylesheet" href="//blueimp.github.io/Gallery/css/blueimp-gallery.min.css">
|
||||
<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload.css">
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui.css">
|
||||
<!-- CSS adjustments for browsers with JavaScript disabled -->
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-noscript.css"></noscript>
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui-noscript.css"></noscript>
|
||||
<style>
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
<% if (is_user_authenticated()) { %>
|
||||
<div><a href="/logout">Logout</a></div>
|
||||
<hr>
|
||||
<input id="fileupload" type="file" name="image" data-url="/upload" multiple>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script src="/file_uploader/js/vendor/jquery.ui.widget.js"></script>
|
||||
<script src="/file_uploader/js/jquery.iframe-transport.js"></script>
|
||||
<script src="/file_uploader/js/jquery.fileupload.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('#fileupload').fileupload({
|
||||
dataType: 'json',
|
||||
done: function (e, data) {
|
||||
$.each(data.result.files, function (index, file) {
|
||||
$('<p/>').text(file.name).appendTo('#lastUploadLog');
|
||||
});
|
||||
},
|
||||
sequentialUploads: true,
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#progress .bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div id="progress">
|
||||
<div class="bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div id="lastUploadLog"></div>
|
||||
<!-- display images from server -->
|
||||
<div>
|
||||
<% foreach my $image (@$images) { %>
|
||||
<div>
|
||||
<hr>
|
||||
<div>
|
||||
<a href='<%= "/$image_base/$orig/$image" %>'>Image original</a>
|
||||
<% for my $scale (@$scales) { %>
|
||||
<a href='<%= "/$image_base/$scale/$image" %>'><%= $scale %></a>
|
||||
<% } %>
|
||||
</div>
|
||||
<div>
|
||||
<img src="<%= "/$image_base/$thumbs_size/$image" %>">
|
||||
</div>
|
||||
<div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<div>
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
<% } %>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
67
lib/FotoStore/DB.pm
Normal file
67
lib/FotoStore/DB.pm
Normal file
|
@ -0,0 +1,67 @@
|
|||
package FotoStore::DB;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use feature qw(signatures);
|
||||
no warnings qw(experimental::signatures);
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $db_file = shift;
|
||||
|
||||
my $dbh = DBI->connect(sprintf('dbi:SQLite:dbname=%s', $db_file),"","");
|
||||
my $self = {
|
||||
dbh => $dbh,
|
||||
};
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub check_user ($self, $nickname, $password) {
|
||||
print STDERR "[$nickname][$password]";
|
||||
my ($user_id) = $self->{'dbh'}->selectrow_array(q~select user_id from users where nickname=? and password=?~, undef, ($nickname, $password));
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
sub get_user ($self, $user_id) {
|
||||
if ($user_id =~ /^\d+$/) {
|
||||
return $self->_get_user_by_user_id($user_id);
|
||||
} else {
|
||||
return $self->_get_user_by_username($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
sub _get_user_by_user_id ($self, $user_id) {
|
||||
my $user_data = $self->{'dbh'}->selectrow_hashref(q~select user_id, nickname, fullname, timestamp from users where user_id=?~, {}, ($user_id));
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
sub _get_user_by_username($self, $username) {
|
||||
my $user_data = $self->{'dbh'}->selectrow_hashref(q~select user_id, nickname, fullname, timestamp from users where nickname=?~, {}, ($username));
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
|
||||
sub add_user($self, $username, $password, $fullname) {
|
||||
my $rows = $self->{'dbh'}->do(q~insert into users (nickname, password, fullname) values (?, ?, ?)~, undef, ($username, $password, $fullname));
|
||||
if ($self->{'dbh'}->errstr) {
|
||||
die $self->{'dbh'}->errstr;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
sub add_file($self, $user_id, $filename) {
|
||||
my $rows = $self->{'dbh'}->do(q~insert into images (owner_id, file_name) values (?, ?)~, undef, ($user_id, $filename));
|
||||
if ($self->{'dbh'}->errstr) {
|
||||
die $self->{'dbh'}->errstr;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub get_files($self, $user_id, $count=20, $start_at=0) {
|
||||
return $self->{'dbh'}->selectall_arrayref(q~select * from images where owner_id=? order by created_time desc~, { Slice => {} }, $user_id );
|
||||
}
|
||||
|
||||
1;
|
9
public/css/main.css
Normal file
9
public/css/main.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
.foto-block {
|
||||
/* border: 1px solid black; */
|
||||
}
|
||||
.foto-block .image {
|
||||
padding: 5px;
|
||||
}
|
||||
.foto-block .foto-notes {
|
||||
padding: 5px;
|
||||
}
|
3
public/file_uploader/.gitignore
vendored
Executable file
3
public/file_uploader/.gitignore
vendored
Executable file
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
*.pyc
|
||||
node_modules
|
81
public/file_uploader/.jshintrc
Executable file
81
public/file_uploader/.jshintrc
Executable file
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens
|
||||
// e.g. `(function () { } ());`
|
||||
"indent" : 4, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
"trailing" : true, // true: Prohibit trailing whitespaces
|
||||
"maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
"maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : false, // {int} Max number of characters per line
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements"
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mootools" : false, // MooTools
|
||||
"node" : false, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Legacy
|
||||
"nomen" : true, // true: Prohibit dangling `_` in variables
|
||||
"onevar" : true, // true: Allow only one `var` statement per function
|
||||
"passfail" : false, // true: Stop on first error
|
||||
"white" : true, // true: Check against strict whitespace and indentation rules
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
20
public/file_uploader/.npmignore
Executable file
20
public/file_uploader/.npmignore
Executable file
|
@ -0,0 +1,20 @@
|
|||
*
|
||||
!css/jquery.fileupload-noscript.css
|
||||
!css/jquery.fileupload-ui-noscript.css
|
||||
!css/jquery.fileupload-ui.css
|
||||
!css/jquery.fileupload.css
|
||||
!img/loading.gif
|
||||
!img/progressbar.gif
|
||||
!js/cors/jquery.postmessage-transport.js
|
||||
!js/cors/jquery.xdr-transport.js
|
||||
!js/vendor/jquery.ui.widget.js
|
||||
!js/jquery.fileupload-angular.js
|
||||
!js/jquery.fileupload-audio.js
|
||||
!js/jquery.fileupload-image.js
|
||||
!js/jquery.fileupload-jquery-ui.js
|
||||
!js/jquery.fileupload-process.js
|
||||
!js/jquery.fileupload-ui.js
|
||||
!js/jquery.fileupload-validate.js
|
||||
!js/jquery.fileupload-video.js
|
||||
!js/jquery.fileupload.js
|
||||
!js/jquery.iframe-transport.js
|
|
@ -1,4 +0,0 @@
|
|||
%syntax-version=1.0.0
|
||||
%project=fotostore
|
||||
%uri=https://rsscp.ru/
|
||||
|
14
sql/deploy/album_images.sql
Normal file
14
sql/deploy/album_images.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:album_images to sqlite
|
||||
-- requires: images
|
||||
-- requires: albums
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE album_images (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
album_id INTEGER REFERENCES albums (album_id),
|
||||
image_id INTEGER REFERENCES images (file_id)
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
14
sql/deploy/albums.sql
Normal file
14
sql/deploy/albums.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:albums to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE albums (
|
||||
album_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name STRING NOT NULL,
|
||||
description TEXT,
|
||||
created DATETIME DEFAULT (CURRENT_TIMESTAMP),
|
||||
modified DATETIME DEFAULT (CURRENT_TIMESTAMP),
|
||||
deleted BOOLEAN
|
||||
);
|
||||
|
||||
COMMIT;
|
17
sql/deploy/images.sql
Normal file
17
sql/deploy/images.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
-- Deploy fotostore:images to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE images (
|
||||
file_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
file_name TEXT NOT NULL,
|
||||
created_time DATETIME NOT NULL
|
||||
DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (
|
||||
owner_id
|
||||
)
|
||||
REFERENCES users (user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
COMMIT;
|
14
sql/deploy/user_albums.sql
Normal file
14
sql/deploy/user_albums.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:user_albums to sqlite
|
||||
-- requires: users
|
||||
-- requires: albums
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE user_albums (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users (user_id),
|
||||
album_id INTEGER REFERENCES albums (album_id)
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
12
sql/deploy/user_images.sql
Normal file
12
sql/deploy/user_images.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
-- Deploy fotostore:user_images to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE user_images (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
image_id INTEGER REFERENCES images (file_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
14
sql/deploy/users.sql
Normal file
14
sql/deploy/users.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:users to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE users (
|
||||
nickname TEXT,
|
||||
password TEXT NOT NULL,
|
||||
fullname TEXT NOT NULL,
|
||||
timestamp DATETIME NOT NULL
|
||||
DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
);
|
||||
|
||||
COMMIT;
|
7
sql/revert/album_images.sql
Normal file
7
sql/revert/album_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:album_images from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE album_images;
|
||||
|
||||
COMMIT;
|
7
sql/revert/albums.sql
Normal file
7
sql/revert/albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:albums from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE albums;
|
||||
|
||||
COMMIT;
|
7
sql/revert/images.sql
Normal file
7
sql/revert/images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:images from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE images;
|
||||
|
||||
COMMIT;
|
7
sql/revert/user_albums.sql
Normal file
7
sql/revert/user_albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:user_albums from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE user_albums;
|
||||
|
||||
COMMIT;
|
7
sql/revert/user_images.sql
Normal file
7
sql/revert/user_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:user_images from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE user_images;
|
||||
|
||||
COMMIT;
|
7
sql/revert/users.sql
Normal file
7
sql/revert/users.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:users from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE users;
|
||||
|
||||
COMMIT;
|
|
@ -6,3 +6,11 @@
|
|||
# target = db:sqlite:
|
||||
# registry = sqitch
|
||||
# client = sqlite3
|
||||
[target "foto_test"]
|
||||
uri = db:sqlite:rsscp_test.db
|
||||
[engine "sqlite"]
|
||||
target = foto_test
|
||||
[deploy]
|
||||
verify = true
|
||||
[rebase]
|
||||
verify = true
|
10
sql/sqitch.plan
Normal file
10
sql/sqitch.plan
Normal file
|
@ -0,0 +1,10 @@
|
|||
%syntax-version=1.0.0
|
||||
%project=fotostore
|
||||
%uri=https://rsscp.ru/
|
||||
|
||||
users 2017-07-22T06:53:03Z Denis Fedoseev <denis.fedoseev@gmail.com> # Creates table to track our users.
|
||||
images 2017-07-22T07:17:52Z Denis Fedoseev <denis.fedoseev@gmail.com> # Creates table to track users images.
|
||||
albums 2017-07-22T08:50:11Z Denis Fedoseev <denis.fedoseev@gmail.com> # Albums database init
|
||||
user_images [users images] 2017-07-22T09:16:20Z Denis Fedoseev <denis.fedoseev@gmail.com> # +user_images table
|
||||
album_images [images albums] 2017-07-22T09:21:13Z Denis Fedoseev <denis.fedoseev@gmail.com> # +images to album mapping
|
||||
user_albums [users albums] 2017-07-22T09:47:48Z Denis Fedoseev <denis.fedoseev@gmail.com> # Albums to users mapping
|
7
sql/verify/album_images.sql
Normal file
7
sql/verify/album_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:album_images on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, album_id, image_id from album_images where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/albums.sql
Normal file
7
sql/verify/albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:albums on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select album_id, name, description, created, modified, deleted from albums where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/images.sql
Normal file
7
sql/verify/images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:images on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select file_id, owner_id, file_name, created_time from images where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/user_albums.sql
Normal file
7
sql/verify/user_albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:user_albums on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, album_id, user_id from user_albums where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/user_images.sql
Normal file
7
sql/verify/user_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:user_images on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, user_id, image_id from user_images where 0;
|
||||
|
||||
ROLLBACK;
|
9
sql/verify/users.sql
Normal file
9
sql/verify/users.sql
Normal file
|
@ -0,0 +1,9 @@
|
|||
-- Verify fotostore:users on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
SELECT user_id, nickname, password, fullname, timestamp
|
||||
FROM users
|
||||
WHERE 0;
|
||||
|
||||
ROLLBACK;
|
10
templates/error.html.ep
Normal file
10
templates/error.html.ep
Normal file
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<%= $message %>
|
||||
</body>
|
||||
</html>
|
||||
|
54
templates/images_list.html.ep
Normal file
54
templates/images_list.html.ep
Normal file
|
@ -0,0 +1,54 @@
|
|||
<div class="container">
|
||||
<div class="logout"><a href="/logout">Logout</a></div>
|
||||
<div class"upload-form">
|
||||
<input id="fileupload" type="file" name="image" data-url="/upload" multiple>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script src="/file_uploader/js/vendor/jquery.ui.widget.js"></script>
|
||||
<script src="/file_uploader/js/jquery.iframe-transport.js"></script>
|
||||
<script src="/file_uploader/js/jquery.fileupload.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('#fileupload').fileupload({
|
||||
dataType: 'json',
|
||||
done: function (e, data) {
|
||||
$.each(data.result.files, function (index, file) {
|
||||
$('<p/>').text(file.name).appendTo('#lastUploadLog');
|
||||
});
|
||||
},
|
||||
sequentialUploads: true,
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#progress .bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div id="progress">
|
||||
<div class="bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div id="lastUploadLog"></div>
|
||||
</div>
|
||||
|
||||
<!-- display images from server -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<% foreach my $image (@$images) { %>
|
||||
<div class="foto-block col-md-3">
|
||||
<div class="image">
|
||||
<img src="<%= "/$image_base/$user_id/$thumbs_size/$image" %>">
|
||||
</div>
|
||||
<div class="foto-notes">
|
||||
<a href='<%= "/$image_base/$user_id/$orig/$image" %>'>Image original</a>
|
||||
<% for my $scale (@$scales) { %>
|
||||
<a href='<%= "/$image_base/$user_id/$scale/$image" %>'><%= $scale %></a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
18
templates/index.html.ep
Normal file
18
templates/index.html.ep
Normal file
|
@ -0,0 +1,18 @@
|
|||
% layout 'base';
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
|
||||
<% if (is_user_authenticated()) { %>
|
||||
%= include 'images_list'
|
||||
<% } else { %>
|
||||
|
||||
<div class="login-form">
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
29
templates/layouts/base.html.ep
Normal file
29
templates/layouts/base.html.ep
Normal file
|
@ -0,0 +1,29 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Bootstrap styles -->
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
|
||||
<!-- Generic page styles -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/style.css">
|
||||
<!-- blueimp Gallery styles -->
|
||||
<link rel="stylesheet" href="//blueimp.github.io/Gallery/css/blueimp-gallery.min.css">
|
||||
<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload.css">
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<!-- CSS adjustments for browsers with JavaScript disabled -->
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-noscript.css"></noscript>
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui-noscript.css"></noscript>
|
||||
<style>
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%= content %>
|
||||
</body>
|
||||
</html>
|
17
templates/no_logged.html.ep
Normal file
17
templates/no_logged.html.ep
Normal file
|
@ -0,0 +1,17 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<div>
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
25
templates/register.html.ep
Normal file
25
templates/register.html.ep
Normal file
|
@ -0,0 +1,25 @@
|
|||
% layout 'base';
|
||||
|
||||
<div class="container">
|
||||
<div class="register">
|
||||
<form method="post" action="<%= url_for('register') %>" >
|
||||
<div>
|
||||
<p>
|
||||
Login:
|
||||
<input type="text" name="username" >
|
||||
</p><p>
|
||||
Fullname:
|
||||
<input type="text" name="Fullname" >
|
||||
</p><p>
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
</p><p>
|
||||
Invite code:
|
||||
<input type="text" name="invite" >
|
||||
</p><p>
|
||||
<input type="submit" value="Register">
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue