Database support #1

Merged
alpha6 merged 8 commits from database_support into master 2017-07-31 10:19:57 +03:00
37 changed files with 692 additions and 243 deletions

2
.gitignore vendored
View file

@ -1,2 +1,2 @@
*.db
*.conf
application.conf

2
README
View file

@ -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>

View file

@ -1,3 +1,4 @@
{
password => '',
db_file => 'sql/fotostore.db',
invite_code => 'very_secure_invite_code',
}

View file

@ -6,3 +6,6 @@ requires 'File::Path';
requires 'File::Spec';
requires 'Cwd';
requires 'Getopt::Long';
requires 'DBI';
requires 'DBD::SQLite';
requires 'Digest::SHA';

361
fotostore.pl Normal file → Executable file
View 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;
},
};
@ -77,10 +65,11 @@ post '/login' => sub {
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 :(' );
}
};
@ -89,32 +78,83 @@ 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 $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");
my $current_user = $self->current_user;
# Sort by new order
@images = sort {$b cmp $a} @images;
my $files_list = $db->get_files($current_user->{'user_id'}, 20);
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(
@ -136,10 +176,10 @@ post '/upload' => (authenticated => 1)=> sub {
# 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."
@ -147,196 +187,81 @@ post '/upload' => (authenticated => 1)=> sub {
}
# 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);
}
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;
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 );
$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';
sub create_hash {
my $data_to_hash = shift;
# Date and time
my ($sec, $min, $hour, $mday, $month, $year) = localtime;
$month = $month + 1;
$year = $year + 1900;
$sha->add($data_to_hash);
return $sha->hexdigest();
}
# Random number(0 ~ 999999)
my $rand_num = int(rand 1000000);
# 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;
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
View 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
View 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
View file

@ -0,0 +1,3 @@
.DS_Store
*.pyc
node_modules

81
public/file_uploader/.jshintrc Executable file
View 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
View 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

View file

@ -1,4 +0,0 @@
%syntax-version=1.0.0
%project=fotostore
%uri=https://rsscp.ru/

View 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
View 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
View 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;

View 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;

View 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
View 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;

View file

@ -0,0 +1,7 @@
-- Revert fotostore:album_images from sqlite
BEGIN;
DROP TABLE album_images;
COMMIT;

7
sql/revert/albums.sql Normal file
View file

@ -0,0 +1,7 @@
-- Revert fotostore:albums from sqlite
BEGIN;
DROP TABLE albums;
COMMIT;

7
sql/revert/images.sql Normal file
View file

@ -0,0 +1,7 @@
-- Revert fotostore:images from sqlite
BEGIN;
DROP TABLE images;
COMMIT;

View file

@ -0,0 +1,7 @@
-- Revert fotostore:user_albums from sqlite
BEGIN;
DROP TABLE user_albums;
COMMIT;

View file

@ -0,0 +1,7 @@
-- Revert fotostore:user_images from sqlite
BEGIN;
DROP TABLE user_images;
COMMIT;

7
sql/revert/users.sql Normal file
View file

@ -0,0 +1,7 @@
-- Revert fotostore:users from sqlite
BEGIN;
DROP TABLE users;
COMMIT;

View file

@ -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
View 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

View 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
View 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
View 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;

View 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;

View 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
View 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
View 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>

View 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
View 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>
<% } %>

View 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>

View 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>

View 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>