├── zss
├── zss.yaml
├── zss.psgi
├── ZSS
│ └── Store.pm
└── ZSS.pm
├── dataserver
├── sv
│ ├── zotero-error
│ │ ├── log
│ │ │ └── run
│ │ └── run
│ ├── zotero-upload
│ │ ├── log
│ │ │ └── run
│ │ └── run
│ └── zotero-download
│ │ ├── log
│ │ └── run
│ │ └── run
├── dbconnect.inc.php
└── config.inc.php
├── mysql
├── zotero.cnf
└── setup_db
├── cert_override.txt
├── patches
├── add_user
└── uwsgi
├── apache
├── dot.htaccess
├── sites-zotero.conf
├── zotero.cert
└── zotero.key
├── README.md
└── Dockerfile
/zss/zss.yaml:
--------------------------------------------------------------------------------
1 | uwsgi:
2 | plugin: psgi
3 | psgi: /srv/zotero/zss/zss.psgi
4 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-error/log/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec svlogd /srv/zotero/log/error
4 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-upload/log/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec svlogd /srv/zotero/log/upload
4 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-download/log/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | exec svlogd /srv/zotero/log/download
4 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-error/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd /srv/zotero/dataserver/processor/error
4 | exec 2>&1
5 | exec chpst -u www-data:www-data php5 daemon.php
6 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-upload/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd /srv/zotero/dataserver/processor/upload
4 | exec 2>&1
5 | exec chpst -u www-data:www-data php5 daemon.php
6 |
--------------------------------------------------------------------------------
/dataserver/sv/zotero-download/run:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd /srv/zotero/dataserver/processor/download
4 | exec 2>&1
5 | exec chpst -u www-data:www-data php5 daemon.php
6 |
--------------------------------------------------------------------------------
/mysql/zotero.cnf:
--------------------------------------------------------------------------------
1 | [mysqld]
2 | character-set-server = utf8
3 | collation-server = utf8_general_ci
4 | event-scheduler = ON
5 | sql-mode = STRICT_ALL_TABLES
6 | default-time-zone = '+0:00'
7 |
--------------------------------------------------------------------------------
/zss/zss.psgi:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl
2 | use strict;
3 | use warnings;
4 |
5 | use lib ('/srv/zotero/zss/');
6 |
7 | use ZSS;
8 |
9 | my $app = ZSS->new();
10 |
11 | $app->psgi_callback();
12 |
--------------------------------------------------------------------------------
/cert_override.txt:
--------------------------------------------------------------------------------
1 | # PSM Certificate Override Settings file
2 | # This is a generated file! Do not edit.
3 | localhost:443 OID.2.16.840.1.101.3.4.2.1 A1:B0:AF:69:BC:F0:59:39:3A:BF:2C:8C:80:05:6B:9F:5F:80:69:BB:12:8C:92:07:C7:B4:E9:2B:90:82:AF:E9 U AAAAAAAAAAAAAAAEAAAAAle12p4wAA==
4 |
--------------------------------------------------------------------------------
/patches/add_user:
--------------------------------------------------------------------------------
1 | #!/usr/bin/php
2 |
3 | set_include_path("../include");
4 | require("header.inc.php");
5 |
6 | if (empty($argv[1]) || empty($argv[2]) || empty($argv[3])) {
7 | die("Usage: $argv[0] " . '$userID $username $password' . "\n");
8 | }
9 |
10 | $userID = $argv[1];
11 | $username = $argv[2];
12 | $password = $argv[3];
13 | $salt = Z_CONFIG::$AUTH_SALT;
14 |
15 | echo "Adding new user $username with ID $userID\n";
16 | Zotero_Users::add($userID, $username);
17 |
18 | $hash = SHA1($salt . $password);
19 | echo "$salt . $password $hash\n";
20 | $sql = "update users set password=? where userid=?";
21 | Zotero_DB::query($sql, array($hash, $userID));
22 | ?>
23 |
--------------------------------------------------------------------------------
/apache/dot.htaccess:
--------------------------------------------------------------------------------
1 | # If on a testing site, deny by default unless IP is allowed
2 | SetEnvIf Host "apidev" ACCESS_CONTROL
3 | SetEnvIf Host "syncdev" ACCESS_CONTROL
4 | ####### Local
5 | SetEnvIf X-Forwarded-For "192.168.1.|" !ACCESS_CONTROL
6 | order deny,allow
7 | deny from env=ACCESS_CONTROL
8 |
9 | #php_flag zlib.output_compression On
10 | #php_value zlib.output_compression_level 5
11 | php_value short_open_tag 1
12 |
13 | php_value include_path "../include"
14 | php_value auto_prepend_file "header.inc.php"
15 | php_value auto_append_file "footer.inc.php"
16 |
17 | php_value memory_limit 500M
18 |
19 | #php_value xdebug.show_local_vars 1
20 | #php_value xdebug.profiler_enable 1
21 | #php_value xdebug.profiler_enable_trigger 1
22 | #php_value xdebug.profiler_output_dir /tmp/xdebug
23 |
24 | RewriteEngine On
25 |
26 | # If file or directory doesn't exist, pass to director for MVC redirections
27 | RewriteCond %{SCRIPT_FILENAME} !-f
28 | RewriteCond %{SCRIPT_FILENAME} !-d
29 | RewriteCond %{REQUEST_URI} !^/zotero
30 | RewriteRule .* index.php [L]
31 |
--------------------------------------------------------------------------------
/mysql/setup_db:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | DB="mysql -h 127.0.0.1 -P 3306 -u root -ppassword"
3 |
4 | echo "DROP DATABASE IF EXISTS zotero_master" | $DB
5 | echo "DROP DATABASE IF EXISTS zotero_shards" | $DB
6 | echo "DROP DATABASE IF EXISTS zotero_ids" | $DB
7 |
8 | echo "CREATE DATABASE zotero_master" | $DB
9 | echo "CREATE DATABASE zotero_shards" | $DB
10 | echo "CREATE DATABASE zotero_ids" | $DB
11 |
12 | echo "DROP USER zotero@localhost;" | $DB
13 |
14 | echo "CREATE USER zotero@localhost IDENTIFIED BY 'foobar';" | $DB
15 |
16 | echo "GRANT SELECT, INSERT, UPDATE, DELETE ON zotero_master.* TO zotero@localhost;" | $DB
17 | echo "GRANT SELECT, INSERT, UPDATE, DELETE ON zotero_shards.* TO zotero@localhost;" | $DB
18 | echo "GRANT SELECT,INSERT,DELETE ON zotero_ids.* TO zotero@localhost;" | $DB
19 |
20 | # Load in master schema
21 | $DB zotero_master < master.sql
22 | $DB zotero_master < coredata.sql
23 |
24 | # Set up shard info
25 | echo "INSERT INTO shardHosts VALUES (1, '127.0.0.1', 3306, 'up');" | $DB zotero_master
26 | echo "INSERT INTO shards VALUES (1, 1, 'zotero_shards', 'up', 0);" | $DB zotero_master
27 |
28 | # Load in shard schema
29 | cat shard.sql | $DB zotero_shards
30 | cat triggers.sql | $DB zotero_shards
31 |
32 | # Load in schema on id server
33 | cat ids.sql | $DB zotero_ids
34 |
--------------------------------------------------------------------------------
/apache/sites-zotero.conf:
--------------------------------------------------------------------------------
1 |
2 | DocumentRoot /srv/web-library
3 |
4 | Options FollowSymLinks
5 | AllowOverride None
6 |
7 |
8 | Options Indexes FollowSymLinks MultiViews
9 | AllowOverride None
10 | Order allow,deny
11 | allow from all
12 |
13 |
14 |
15 |
16 |
17 | DocumentRoot /srv/zotero/dataserver/htdocs
18 | SSLEngine on
19 | SSLCertificateFile /etc/apache2/zotero.cert
20 | SSLCertificateKeyFile /etc/apache2/zotero.key
21 |
22 |
23 | SetHandler uwsgi-handler
24 | uWSGISocket /var/run/uwsgi/app/zss/socket
25 | uWSGImodifier1 5
26 |
27 |
28 |
29 | Options FollowSymLinks MultiViews
30 | AllowOverride All
31 |
32 | #2.2
33 | Order allow,deny
34 | Allow from all
35 | # 2.4
36 | # Require all granted
37 | #
38 | # If you are using a more recent version of apache
39 | # and are getting 403 errors, replace the Order and
40 | # Allow lines with:
41 | # Require all granted
42 |
43 |
44 | ErrorLog /srv/zotero/log/error.log
45 | CustomLog /srv/zotero/log/access.log common
46 |
47 |
--------------------------------------------------------------------------------
/dataserver/dbconnect.inc.php:
--------------------------------------------------------------------------------
1 |
2 | function Zotero_dbConnectAuth($db) {
3 | $charset = '';
4 |
5 | if ($db == 'master') {
6 | $host = 'localhost';
7 | $port = 3306;
8 | $db = 'zotero_master';
9 | $user = 'zotero';
10 | $pass = 'foobar';
11 | }
12 | else if ($db == 'shard') {
13 | $host = false;
14 | $port = false;
15 | $db = false;
16 | $user = 'zotero';
17 | $pass = 'foobar';
18 | }
19 | else if ($db == 'id1') {
20 | $host = 'localhost';
21 | $port = 3306;
22 | $db = 'zotero_ids';
23 | $user = 'zotero';
24 | $pass = 'foobar';
25 | }
26 | else if ($db == 'id2') {
27 | $host = 'localhost';
28 | $port = 3306;
29 | $db = 'zotero_ids';
30 | $user = 'zotero';
31 | $pass = 'foobar';
32 | }
33 | else {
34 | throw new Exception("Invalid db '$db'");
35 | }
36 | return array(
37 | 'host'=>$host,
38 | 'port'=>$port,
39 | 'db'=>$db,
40 | 'user'=>$user,
41 | 'pass'=>$pass,
42 | 'charset'=>$charset
43 | );
44 | }
45 | ?>
46 |
--------------------------------------------------------------------------------
/apache/zotero.cert:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEFTCCAmegAwIBAgIEV7XanjANBgkqhkiG9w0BAQsFADAAMB4XDTE2MDgxODE1
3 | NTYxNloXDTQ0MDEwNDE1NTYxOFowADCCAbgwDQYJKoZIhvcNAQEBBQADggGlADCC
4 | AaACggGXANJ8/OlJsFwD0I2Xgjas22dg5ESYcj+xf5IBNd1FVQxKUfpLA/vpEV9n
5 | bIyDHZgXdiKKQYdfTUbdJVX7ilSmmvDJ9wPjfa/72L7fCJl2oCY98pNVNBelXe/u
6 | zABW4PZVGNoaLE/H5/Bar14E9l0YJ6DbaaEQ8xNeieTZkGZ0SUwVdC0p6V11dEHG
7 | MPhEw3aXH0kAy9KAAPmkXVnmSKsRnCaVft1ob8IMvYPmHwlYa4uYbW3JhOu8NkMJ
8 | kVWJ3JEdWfWPX/M7VT7DFyC42JP17NHJgViFS8tuYGiWtwFDeY1grExB3ZDFySGh
9 | s3NYSMRV710xAY5M7oqKnojv53tGzeQOQ5Mhv1A9i62f4h3xzwnQ7rroPprU5h4Y
10 | LPvIQ4RnVOH1o7+Ug70SG8IOWPP8wX8egBWalWqtrP5XyADEUwZM3f0EWUUMnCg1
11 | kZp1woiE4eRI6YdbWBrJlelcF6DSo+GONHHnyp438PS9eiLE0Xmv4ZyRXx+TjIMC
12 | A1RqydW6qsHmvaBuAwCKggoFvQ5vSpAolgBFIOcCAwEAAaNrMGkwDAYDVR0TAQH/
13 | BAIwADAlBgNVHREEHjAcgglsb2NhbGhvc3SCCWxvY2FsaG9zdIcEfwAAATATBgNV
14 | HSUEDDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUTnN76df4/7g8WmMd7J2UTIUO/wcw
15 | DQYJKoZIhvcNAQELBQADggGXAM1Gdi5xk74RPJTIwdRn/J7YLQMwwUB0Nhsk/FvO
16 | 9pjQJrXBNq7dPHaLPwpFCK6YjDiVm3rV7f96mdSpo7A9GtH0d2Cx6I1a+3h/zHUu
17 | XSWLoLPQpt84Qk2qhNRNNQhBc6NbpSS/754wQH4o+QiVbfrti2f0SaGAlso1tJbB
18 | /gckYA9SjCDlBPL4nUjms2w32ooiXxUkYcgAp0a9TBpQ6YgNL5bD3m0I2kPi/VZD
19 | UIaMoj6lw5xUKaT8EhowrwO5796bCBB3sCGN0YciUrMrl04ZWYqbNuZYtFCb2kWt
20 | K9wkiIoZzn+CMEfDFmFODG4qf0YvwO+qZz26ph4gJJ6XIi8vcZJam9HBWBvb/TOZ
21 | UZVZKwHvTPkVFLyCd8q8S3LPlSrvWy4m63jtp6FGbOt0lQyVEt/L/xsig72UXBxl
22 | 3QfdPEHbr5pL8L8HNHElT2SB8q2OKFpHSdQtwA45jCA7W3OHtCIYvIhf34fAIHmX
23 | 2rglG/EHHGCZSPaC6lzbAyXvcuGyQ5ENUQiEL84nNm1vo51p6zlzR/E=
24 | -----END CERTIFICATE-----
25 |
--------------------------------------------------------------------------------
/apache/zotero.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | 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
41 | -----END RSA PRIVATE KEY-----
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Look at https://github.com/SamuelHassine/zotero-prime which is more frequently maintained than this repo**
2 |
3 | ------------------------------
4 |
5 | # Docker image for Zotero Data Server
6 |
7 | This image was build following the instructions for installing a Zotero dataserver at (http://git.27o.de/dataserver/about/), which is an updated procedure of [this document](https://github.com/Panzerkampfwagen/dataserver/blob/master/misc/Zotero_Data_Server_Installation_Debian.pdf).
8 |
9 |
10 | ## Build the image
11 |
12 | docker build -t zotero .
13 |
14 | The resulting image is configured to run a dataserver on https://localhost/.
15 |
16 | To customize the installation the following files must be edited:
17 | * SSL certificate: `apache/zotero.{cert,key}`. The current certificate is self-signed for localhost.
18 | * Apache site config: `apache/sites-zotero.conf`.
19 | * Dataserver: `dataserver/config.inc.php` to match the site config.
20 | * MySQL credentials/passwords: `mysql/setup\_db` and `dataserver/dbconnect.inc.php` accordingly.
21 |
22 | The build procedure also creates a couple of test users using the user administration tools: test:test and test2:test2.
23 |
24 |
25 | ## Start the dataserver
26 |
27 | # 1st run. Named container simplifies the access (to the data) across runs
28 | docker run -p 80:80 -p 443:443 --name=FOO -t -i zotero
29 | # all the subsequent runs
30 | docker start FOO; docker attach FOO
31 |
32 | This will start the dataserver on [https://localhost/](https://localhost/sync/login?version=9&username=test&password=test). Because of the self-signed certificate some browsers may refuse to connect to the server.
33 |
34 |
35 | ## Patch the standalone client to use the new dataserver
36 |
37 | Following the procedure of (http://git.27o.de/dataserver/about/Zotero-Client.md).
38 | Download the Zotero client, and change these two lines in `resource/config.js` inside the zotero.jar archive (zip)
39 |
40 | SYNC_URL: 'https://localhost/sync/',
41 | API_URL: 'https://localhost/',
42 |
43 | If the server uses a self-signed certificate an exception should be added to the client. A `cert\_override.txt` file must be added to the user profile generated by zotero client:
44 |
45 | ~/Library/Application\ Support/Zotero/Profiles/.default/ MAC
46 | ~/.zotero/Profiles/.default/ Linux
47 | c:Users//AppData/Roaming/Zotero/Zotero/ Win
48 |
49 | The `cert\_override.txt` file can be generated with Firefox as explained here (https://groups.google.com/d/msg/zotero-dev/MEwLaptJIzI/PVDAFJiqEgAJ). The override file in this directory corresponds to the self-signed certificate in the apache directory.
50 |
51 |
52 | ## User administration
53 |
54 | cd /srv/zotero/dataserver/admin
55 | ./add_user 101 testuser testpassword
56 | ./add_user 102 testuser2 testpassword2
57 | ./add_group -o testuser -f members -r members -e members testgroup
58 | ./add_groupuser testgroup testuser2 member
59 |
60 | add\_user is a patched version of the script from http://git.27o.de that allows to set the password from the command line.
61 |
62 |
63 |
--------------------------------------------------------------------------------
/dataserver/config.inc.php:
--------------------------------------------------------------------------------
1 |
2 | class Z_CONFIG {
3 | public static $API_ENABLED = true;
4 | public static $SYNC_ENABLED = true;
5 | public static $PROCESSORS_ENABLED = true;
6 | public static $MAINTENANCE_MESSAGE = 'Server updates in progress. Please try again in a few minutes.';
7 |
8 | public static $TESTING_SITE = false;
9 | public static $DEV_SITE = false;
10 |
11 | public static $DEBUG_LOG = false;
12 |
13 | public static $BASE_URI = 'http://zotero.org/';
14 | public static $API_BASE_URI = 'https://localhost/';
15 | public static $WWW_BASE_URI = '';
16 | public static $SYNC_DOMAIN = 'sync';
17 |
18 | public static $AUTH_SALT = 'sometext';
19 | public static $API_SUPER_USERNAME = 'someusername';
20 | public static $API_SUPER_PASSWORD = 'somepassword';
21 |
22 | public static $AWS_ACCESS_KEY = '';
23 | public static $AWS_SECRET_KEY = 'yoursecretkey';
24 | public static $S3_BUCKET = 'zotero';
25 | public static $S3_ENDPOINT = 'localhost';
26 | public static $S3_USE_SSL = true;
27 | public static $S3_VALIDATE_SSL = false;
28 |
29 | public static $URI_PREFIX_DOMAIN_MAP = array(
30 | '/sync/' => 'sync'
31 | );
32 |
33 | public static $MEMCACHED_ENABLED = true;
34 | public static $MEMCACHED_SERVERS = array(
35 | 'localhost:11211'
36 | );
37 |
38 | public static $TRANSLATION_SERVERS = array(
39 | "translation1.localdomain:1969"
40 | );
41 |
42 | public static $CITATION_SERVERS = array(
43 | "citeserver1.localdomain:8080", "citeserver2.localdomain:8080"
44 | );
45 |
46 | public static $ATTACHMENT_SERVER_HOSTS = array("files1.localdomain", "files2.localdomain");
47 | public static $ATTACHMENT_SERVER_DYNAMIC_PORT = 80;
48 | public static $ATTACHMENT_SERVER_STATIC_PORT = 81;
49 | public static $ATTACHMENT_SERVER_URL = "https://files.example.net";
50 | public static $ATTACHMENT_SERVER_DOCROOT = "/var/www/attachments/";
51 |
52 | public static $STATSD_ENABLED = false;
53 | public static $STATSD_PREFIX = "";
54 | public static $STATSD_HOST = "monitor.localdomain";
55 | public static $STATSD_PORT = 8125;
56 |
57 | public static $LOG_TO_SCRIBE = false;
58 | public static $LOG_ADDRESS = '';
59 | public static $LOG_PORT = 1463;
60 | public static $LOG_TIMEZONE = 'US/Eastern';
61 | public static $LOG_TARGET_DEFAULT = 'errors';
62 |
63 | public static $PROCESSOR_PORT_DOWNLOAD = 3455;
64 | public static $PROCESSOR_PORT_UPLOAD = 3456;
65 | public static $PROCESSOR_PORT_ERROR = 3457;
66 |
67 | public static $PROCESSOR_LOG_TARGET_DOWNLOAD = 'sync-processor-download';
68 | public static $PROCESSOR_LOG_TARGET_UPLOAD = 'sync-processor-upload';
69 | public static $PROCESSOR_LOG_TARGET_ERROR = 'sync-processor-error';
70 |
71 | public static $SYNC_DOWNLOAD_SMALLEST_FIRST = false;
72 | public static $SYNC_UPLOAD_SMALLEST_FIRST = false;
73 |
74 | // Set some things manually for running via command line
75 | public static $CLI_PHP_PATH = '/usr/bin/php';
76 | public static $CLI_DOCUMENT_ROOT = "/srv/zotero/dataserver/";
77 |
78 | public static $SYNC_ERROR_PATH = '/srv/zotero/log/sync-errors/';
79 | public static $API_ERROR_PATH = '/srv/zotero/log/api-errors/';
80 |
81 | public static $CACHE_VERSION_ATOM_ENTRY = 1;
82 | public static $CACHE_VERSION_BIB = 1;
83 | public static $CACHE_VERSION_ITEM_DATA = 1;
84 | }
85 | ?>
86 |
--------------------------------------------------------------------------------
/zss/ZSS/Store.pm:
--------------------------------------------------------------------------------
1 | package ZSS::Store;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use Digest::MD5 qw (md5_hex);
7 | use File::Util qw(escape_filename);
8 | use File::Path qw(make_path);
9 |
10 | sub new {
11 | my $class = shift;
12 |
13 | # TODO: read from config
14 | my $self = {storagepath => shift};
15 |
16 | bless $self, $class;
17 | }
18 |
19 | sub get_path {
20 | my $self = shift;
21 | my $key = shift;
22 |
23 | my $dirname = md5_hex($key);
24 |
25 | my $dir = $self->{storagepath} . substr($dirname, 0, 1) . "/" . $dirname ."/";
26 |
27 | return $dir;
28 | }
29 |
30 | sub get_filename {
31 | my $self = shift;
32 | my $key = shift;
33 |
34 | return escape_filename($key, '_');
35 | }
36 |
37 | sub get_filepath {
38 | my $self = shift;
39 | my $key = shift;
40 |
41 | return $self->get_path($key) . $self->get_filename($key);
42 | }
43 |
44 | sub store_file {
45 | my $self = shift;
46 | my $key = shift;
47 | my $data = shift;
48 | my $meta = shift;
49 |
50 | my $dir = $self->get_path($key);
51 | my $file = $self->get_filename($key);
52 |
53 | make_path($dir);
54 |
55 | # Write data to temp file and rename to the desired name
56 | # This only changes this file and not other hardlinks
57 | open(my $fh, '>:raw', $dir.$file.".temp");
58 | print $fh ($data);
59 | close($fh);
60 | rename($dir.$file.".temp", $dir.$file);
61 |
62 | if ($meta) {
63 | open($fh, '>:raw', $dir.$file.".meta.temp");
64 | print $fh ($meta);
65 | close($fh);
66 | rename($dir.$file.".meta.temp", $dir.$file.".meta");
67 | }
68 | }
69 |
70 | sub check_exists{
71 | my $self = shift;
72 | my $key = shift;
73 |
74 | my $path = $self->get_filepath($key);
75 | unless (-e $path){
76 | return 0;
77 | }
78 | return 1;
79 | }
80 |
81 | sub retrieve_file {
82 | my $self = shift;
83 | my $key = shift;
84 |
85 | unless($self->check_exists($key)){
86 | return undef;
87 | }
88 | my $path = $self->get_filepath($key);
89 | open(my $fh, '<:raw', $path);
90 | return $fh;
91 | }
92 |
93 | sub retrieve_filemeta {
94 | my $self = shift;
95 | my $key = shift;
96 |
97 | unless($self->check_exists($key)){
98 | return undef;
99 | }
100 | my $metafile = $self->get_filepath($key) . ".meta";
101 |
102 | # check if metadata is present
103 | unless (-e $metafile) {
104 | return undef;
105 | }
106 |
107 | # limt size of metadata to 8kB
108 | my $size = -s $metafile;
109 | unless ($size <= 8192) {
110 | return undef;
111 | }
112 |
113 | my $meta;
114 | open(my $fh, '<:raw', $metafile);
115 | read ($fh, $meta, $size);
116 | return $meta;
117 | }
118 |
119 | sub get_size{
120 | my $self = shift;
121 | my $key = shift;
122 |
123 | my $path = $self->get_filepath($key);
124 |
125 | unless (-e $path) {
126 | return 0;
127 | }
128 | my $size = -s $path;
129 | return $size;
130 | }
131 |
132 | sub link_files{
133 | my $self = shift;
134 | my $source_key = shift;
135 | my $destination_key = shift;
136 |
137 | my $source_path = $self->get_filepath($source_key);
138 | my $destination_dir = $self->get_path($destination_key);
139 | my $destination_path = $self->get_filepath($destination_key);
140 |
141 | make_path($destination_dir);
142 |
143 | link($source_path.".meta", $destination_path.".meta");
144 |
145 | return link($source_path, $destination_path);
146 | }
147 |
148 | sub delete_file{
149 | my $self = shift;
150 | my $key = shift;
151 |
152 | my $dir = $self->get_path($key);
153 | my $file = $self->get_filename($key);
154 |
155 | # Remove metadata
156 | unlink($dir.$file.".meta");
157 |
158 | unless (unlink($dir.$file)) {
159 | return 1;
160 | }
161 | return rmdir($dir);
162 | }
163 |
164 | 1;
165 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:wheezy-backports
2 | MAINTAINER Gabriele Facciolo
3 | # Following http://git.27o.de/dataserver/about/Installation-Instructions-for-Debian-Wheezy.md
4 |
5 | # debian packages
6 | RUN apt-get update && apt-get install -y \
7 | apache2 libapache2-mod-php5 mysql-server memcached zendframework php5-cli php5-memcached php5-mysql php5-curl \
8 | apache2 uwsgi uwsgi-plugin-psgi libplack-perl libdigest-hmac-perl libjson-xs-perl libfile-util-perl libapache2-mod-uwsgi libswitch-perl \
9 | git gnutls-bin runit wget curl net-tools vim build-essential
10 |
11 | # Zotero
12 | RUN mkdir -p /srv/zotero/log/upload && \
13 | mkdir -p /srv/zotero/log/download && \
14 | mkdir -p /srv/zotero/log/error && \
15 | mkdir -p /srv/zotero/log/api-errors && \
16 | mkdir -p /srv/zotero/log/sync-errors && \
17 | mkdir -p /srv/zotero/dataserver && \
18 | mkdir -p /srv/zotero/zss && \
19 | mkdir -p /var/log/httpd/sync-errors && \
20 | mkdir -p /var/log/httpd/api-errors && \
21 | chown www-data: /var/log/httpd/sync-errors && \
22 | chown www-data: /var/log/httpd/api-errors
23 |
24 | # Dataserver
25 | RUN git clone --depth=1 git://git.27o.de/dataserver /srv/zotero/dataserver && \
26 | chown www-data:www-data /srv/zotero/dataserver/tmp
27 | #RUN cd /srv/zotero/dataserver/include && rm -r Zend && ln -s /usr/share/php/libzend-framework-php/Zend
28 | RUN cd /srv/zotero/dataserver/include && rm -r Zend && ln -s /usr/share/php/Zend
29 |
30 | #Apache2
31 | #certtool -p --sec-param high --outfile /etc/apache2/zotero.key
32 | #certtool -s --load-privkey /etc/apache2/zotero.key --outfile /etc/apache2/zotero.cert
33 | ADD apache/zotero.key /etc/apache2/
34 | ADD apache/zotero.cert /etc/apache2/
35 | ADD apache/sites-zotero.conf /etc/apache2/sites-available/zotero
36 | ADD apache/dot.htaccess /srv/zotero/dataserver/htdocs/\.htaccess
37 | RUN a2enmod ssl && \
38 | a2enmod rewrite && \
39 | a2ensite zotero
40 |
41 | #Mysql
42 | ADD mysql/zotero.cnf /etc/mysql/conf.d/zotero.cnf
43 | ADD mysql/setup_db /srv/zotero/dataserver/misc/setup_db
44 | RUN /etc/init.d/mysql start && \
45 | mysqladmin -u root password password && \
46 | cd /srv/zotero/dataserver/misc/ && \
47 | ./setup_db
48 |
49 |
50 | # Zotero Configuration
51 | ADD dataserver/dbconnect.inc.php dataserver/config.inc.php /srv/zotero/dataserver/include/config/
52 | ADD dataserver/sv/zotero-download /etc/sv/zotero-download
53 | ADD dataserver/sv/zotero-upload /etc/sv/zotero-upload
54 | ADD dataserver/sv/zotero-error /etc/sv/zotero-error
55 | RUN cd /etc/service && \
56 | ln -s ../sv/zotero-download /etc/service/ && \
57 | ln -s ../sv/zotero-upload /etc/service/ && \
58 | ln -s ../sv/zotero-error /etc/service/
59 |
60 |
61 |
62 | # ZSS
63 | RUN git clone --depth=1 git://git.27o.de/zss /srv/zotero/zss && \
64 | mkdir /srv/zotero/storage && \
65 | chown www-data:www-data /srv/zotero/storage
66 |
67 | ADD zss/zss.yaml /etc/uwsgi/apps-available/
68 | ADD zss/ZSS.pm /srv/zotero/zss/
69 | ADD zss/zss.psgi /srv/zotero/zss/
70 | RUN ln -s /etc/uwsgi/apps-available/zss.yaml /etc/uwsgi/apps-enabled
71 | # fix uwsgi init scipt (always fails)
72 | ADD patches/uwsgi /etc/init.d/uwsgi
73 |
74 |
75 | ## failed attempt to install Zotero Web-Library locally
76 | ## not working
77 | #RUN cd /srv/ && \
78 | # git clone --depth=1 --recursive https://github.com/zotero/web-library.git && \
79 | # curl -sL https://deb.nodesource.com/setup_4.x | bash - && apt-get install -y nodejs && \
80 | # cd /srv/web-library && \
81 | # npm install && \
82 | # npm install prompt
83 |
84 |
85 |
86 |
87 | # replace custom /srv/zotero/dataserver/admin/add_user that allows to write the password
88 | ADD patches/add_user /srv/zotero/dataserver/admin/add_user
89 |
90 | # TEST ADD USER: test PASSWORD: test
91 | RUN service mysql start && service memcached start && \
92 | cd /srv/zotero/dataserver/admin && \
93 | ./add_user 101 test test && \
94 | ./add_user 102 test2 test2 && \
95 | ./add_group -o test -f members -r members -e members testgroup && \
96 | ./add_groupuser testgroup test2 member
97 |
98 |
99 | # docker server startup
100 | EXPOSE 80 443
101 |
102 | CMD service mysql start && \
103 | service uwsgi start && \
104 | service apache2 start && \
105 | service memcached start && \
106 | bash -c "/usr/sbin/runsvdir-start&" && \
107 | /bin/bash
108 |
--------------------------------------------------------------------------------
/patches/uwsgi:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ### BEGIN INIT INFO
3 | # Provides: uwsgi
4 | # Required-Start: $local_fs $remote_fs $network
5 | # Required-Stop: $local_fs $remote_fs $network
6 | # Default-Start: 2 3 4 5
7 | # Default-Stop: 0 1 6
8 | # Short-Description: Start/stop uWSGI server instance(s)
9 | # Description: This script manages uWSGI server instance(s).
10 | # You could control specific instance(s) by issuing:
11 | #
12 | # service uwsgi ...
13 | #
14 | # You can issue to init.d script following commands:
15 | # * start | starts daemon
16 | # * stop | stops daemon
17 | # * reload | sends to daemon SIGHUP signal
18 | # * force-reload | sends to daemon SIGTERM signal
19 | # * restart | issues 'stop', then 'start' commands
20 | # * status | shows status of daemon instance
21 | #
22 | # 'status' command must be issued with exactly one
23 | # argument: ''.
24 | #
25 | # In init.d script output:
26 | # * . -- command was executed without problems or instance
27 | # is already in needed state
28 | # * ! -- command failed (or executed with some problems)
29 | # * ? -- configuration file for this instance isn't found
30 | # and this instance is ignored
31 | #
32 | # For more details see /usr/share/doc/uwsgi/README.Debian.
33 | ### END INIT INFO
34 |
35 | # Author: Leonid Borisenko
36 |
37 | # PATH should only include /usr/* if it runs after the mountnfs.sh script
38 | PATH=/sbin:/usr/sbin:/bin:/usr/bin
39 | DESC="app server(s)"
40 | NAME="uwsgi"
41 | DAEMON="/usr/bin/uwsgi"
42 | SCRIPTNAME="/etc/init.d/${NAME}"
43 |
44 | UWSGI_CONFDIR="/etc/uwsgi"
45 | UWSGI_APPS_CONFDIR_SUFFIX="s-enabled"
46 | UWSGI_APPS_CONFDIR_GLOB="${UWSGI_CONFDIR}/app${UWSGI_APPS_CONFDIR_SUFFIX}"
47 |
48 | UWSGI_RUNDIR="/run/uwsgi"
49 |
50 | # Configuration namespace is used as name of runtime and log subdirectory.
51 | # uWSGI instances sharing the same app configuration directory also shares
52 | # the same runtime and log subdirectory.
53 | #
54 | # When init.d script cannot detect namespace for configuration file, default
55 | # namespace will be used.
56 | UWSGI_DEFAULT_CONFNAMESPACE=app
57 |
58 | # Exit if the package is not installed
59 | [ -x "$DAEMON" ] || exit 0
60 |
61 | # Load the VERBOSE setting and other rcS variables
62 | . /lib/init/vars.sh
63 |
64 | # Read configuration variable file if it is present
65 | [ -r "/etc/default/${NAME}" ] && . "/etc/default/${NAME}"
66 |
67 | # Define LSB log_* functions.
68 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
69 | . /lib/lsb/init-functions
70 |
71 | # Define supplementary functions
72 | . /usr/share/uwsgi/init/snippets
73 | . /usr/share/uwsgi/init/do_command
74 |
75 | WHAT=$1
76 | shift
77 | case "$WHAT" in
78 | start)
79 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
80 | do_command "$WHAT" "$@"
81 | RETVAL="$?"
82 | RETVAL=0
83 | [ "$VERBOSE" != no ] && log_end_msg "$RETVAL"
84 | ;;
85 |
86 | stop)
87 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
88 | do_command "$WHAT" "$@"
89 | RETVAL="$?"
90 | [ "$VERBOSE" != no ] && log_end_msg "$RETVAL"
91 | ;;
92 |
93 | status)
94 | if [ -z "$1" ]; then
95 | [ "$VERBOSE" != no ] && log_failure_msg "which one?"
96 | else
97 | PIDFILE="$(
98 | find_specific_pidfile "$(relative_path_to_conffile_with_spec "$1")"
99 | )"
100 | status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" \
101 | && exit 0 \
102 | || exit $?
103 | fi
104 | ;;
105 |
106 | reload)
107 | [ "$VERBOSE" != no ] && log_daemon_msg "Reloading $DESC" "$NAME"
108 | do_command "$WHAT" "$@"
109 | RETVAL="$?"
110 | [ "$VERBOSE" != no ] && log_end_msg "$RETVAL"
111 | ;;
112 |
113 | force-reload)
114 | [ "$VERBOSE" != no ] && log_daemon_msg "Forced reloading $DESC" "$NAME"
115 | do_command "$WHAT" "$@"
116 | RETVAL="$?"
117 | [ "$VERBOSE" != no ] && log_end_msg "$RETVAL"
118 | ;;
119 |
120 | restart)
121 | [ "$VERBOSE" != no ] && log_daemon_msg "Restarting $DESC" "$NAME"
122 | CURRENT_VERBOSE=$VERBOSE
123 | VERBOSE=no
124 | do_command stop "$@"
125 | VERBOSE=$CURRENT_VERBOSE
126 | case "$?" in
127 | 0)
128 | do_command start "$@"
129 | RETVAL="$?"
130 | [ "$VERBOSE" != no ] && log_end_msg "$RETVAL"
131 | ;;
132 | *)
133 | # Failed to stop
134 | [ "$VERBOSE" != no ] && log_end_msg 1
135 | ;;
136 | esac
137 | ;;
138 |
139 | *)
140 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2
141 | exit 3
142 | ;;
143 | esac
144 |
--------------------------------------------------------------------------------
/zss/ZSS.pm:
--------------------------------------------------------------------------------
1 | package ZSS;
2 |
3 | use strict;
4 | use warnings;
5 |
6 | use Plack::Request;
7 | use Digest::HMAC_SHA1 qw(hmac_sha1);
8 | use Digest::MD5 qw (md5_base64);
9 | use MIME::Base64 qw(decode_base64 encode_base64);
10 | use JSON::XS;
11 | use Date::Parse;
12 | use URI;
13 | use URI::QueryParam;
14 | use URI::Escape;
15 | use Switch;
16 | use Encode;
17 | use Try::Tiny;
18 |
19 | use ZSS::Store;
20 |
21 | use Data::Dumper qw(Dumper);
22 | $Data::Dumper::Sortkeys = 1;
23 |
24 | sub new {
25 | my ($class) = @_;
26 |
27 | # TODO: read from config
28 | my $self = {};
29 |
30 | $self->{buckets}->{zotero}->{secretkey} = "yoursecretkey";
31 | $self->{buckets}->{zotero}->{store} = ZSS::Store->new("/srv/zotero/storage/");
32 |
33 | bless $self, $class;
34 | }
35 |
36 | sub respond {
37 | my $code = shift;
38 | my $msg = shift;
39 | return [ $code, [ 'Content-Type' => 'text/plain', 'Content-Length' => length($msg)], [$msg] ];
40 | }
41 |
42 | sub xml2string {
43 | my $xml = shift;
44 |
45 | my $msg = '';
46 |
47 | while (my $token = shift @{$xml}) {
48 | my $data = shift @{$xml};
49 | $msg .= '<'.$token.'>';
50 | if (ref $data eq 'ARRAY') {
51 | $msg .= xml2string($data);
52 | } else {
53 | $msg .= $data;
54 | }
55 | $msg .= ''.$token.'>';
56 | }
57 | return $msg;
58 | }
59 |
60 | sub respondXML {
61 | my $code = shift;
62 | my $xml = shift;
63 |
64 | return [ $code, [ 'Content-Type' => 'application/xml'], ["\n".xml2string($xml)] ];
65 | }
66 |
67 | sub check_policy {
68 | my ($self, $cmp, $key, $val) = @_;
69 |
70 | switch ($cmp) {
71 | case 'eq' {
72 | if ($key eq 'bucket') {
73 | $key = $self->{request}->{bucket};
74 | } else {
75 | # TODO: Replace Plack::Request
76 | $key = $self->{req}->parameters->get($key)
77 | }
78 | return 1 if $key eq $val;
79 | };
80 | case 'content-length-range' {
81 | my $len = $self->{request}->{env}->{CONTENT_LENGTH};
82 | # $self->log("Length: ".$len.", Limits: ".$key.", ".$val);
83 | return 1 if (($len > $key) && ($len < $val));
84 | }
85 | }
86 | return 0;
87 | }
88 |
89 | sub log {
90 | my ($self, $msg) = @_;
91 |
92 | $self->{request}->{env}->{'psgix.logger'}->({ level => 'debug', message => $msg });
93 | }
94 |
95 | sub get_signature {
96 | my $self = shift;
97 |
98 | my $request = $self->{request};
99 | my $env = $request->{env};
100 |
101 | my $secret = $self->{buckets}->{$request->{bucket}}->{secretkey};
102 |
103 | my $query = {};
104 | my $use_query = undef;
105 |
106 | if ($env->{QUERY_STRING}) {
107 | $query = $request->{uri}->query_form_hash();
108 | }
109 |
110 | if ($query->{Signature}) {
111 | $use_query = 1;
112 | }
113 |
114 | # X-AMZ headers or query parameters
115 | my $amzstring = '';
116 | if ($use_query) {
117 | for my $key (sort(grep(/^x-amz/, keys %$query))) {
118 | my $value = $query->{$key};
119 | $amzstring .= lc($key).":".$value."\n";
120 | }
121 | } else {
122 | for my $key (sort(grep(/^HTTP_X_AMZ/, keys %$env))) {
123 | next if ($key eq 'HTTP_X_AMZ_DATE');
124 | my $value = $env->{$key};
125 | $key =~ s/_/-/g;
126 | $amzstring .= lc(substr($key,5)).":".$value."\n";
127 | }
128 | }
129 |
130 | # Date Header or Expires query parameter
131 | my $date;
132 | if ($use_query) {
133 | $date = $query->{Expires};
134 | } else {
135 | if ($env->{HTTP_X_AMZ_DATE}) {
136 | $date = $env->{HTTP_X_AMZ_DATE};
137 | } else {
138 | $date = $env->{HTTP_DATE};
139 | }
140 | }
141 |
142 | # changing response headers parameters
143 | my $additional_params = '';
144 | if ($query) {
145 | my $sep = '?';
146 | my @params = qw(response-cache-control response-content-disposition response-content-encoding response-content-language response-content-type response-expires);
147 | for my $key (@params) {
148 | if ($query->{$key}) {
149 | $additional_params .= $sep.$key."=".$query->{$key};
150 | $sep = '&' if ($sep eq '?');
151 | }
152 | }
153 | }
154 |
155 | my $stringtosign = $env->{REQUEST_METHOD}."\n".
156 | ($env->{HTTP_CONTENT_MD5} || '')."\n".
157 | ($env->{CONTENT_TYPE} || '')."\n".
158 | ($date || '')."\n".
159 | $amzstring.
160 | "/".$request->{bucket}."/".$request->{key_escaped}.$additional_params;
161 |
162 | # $self->log("Stringtosign:".$stringtosign."End");
163 |
164 | return encode_base64(hmac_sha1($stringtosign, $secret), '');
165 | }
166 |
167 | sub check_signature {
168 | my $self = shift;
169 |
170 | my $request = $self->{request};
171 | my $env = $request->{env};
172 |
173 | my $received_signature;
174 |
175 | if ($env->{QUERY_STRING} eq '') {
176 | ($received_signature) = ($env->{HTTP_AUTHORIZATION} || '') =~ m/^AWS .*:(.*)$/;
177 | } else {
178 | $received_signature = $request->{uri}->query_param('Signature') || '';
179 | }
180 |
181 | unless ($received_signature) {
182 | return 0;
183 | }
184 |
185 | my $signature = $self->get_signature();
186 |
187 | # $self->log("Check Signature: $received_signature == $signature");
188 |
189 | return ($signature eq $received_signature);
190 | }
191 |
192 | sub handle_POST {
193 | my ($self) = @_;
194 |
195 | my $request = $self->{request};
196 | my $env = $request->{env};
197 |
198 | my $req = $self->{req};
199 |
200 | my $policy = $req->parameters->get('policy');
201 | my $signature = $req->parameters->get('signature');
202 |
203 | unless ($signature && $policy) {
204 | return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']]);
205 | }
206 |
207 | unless ($signature eq encode_base64(hmac_sha1($policy, $self->{buckets}->{$request->{bucket}}->{secretkey}), '')) {
208 | return respondXML(403, ['Error' => ['Code' => 'SignatureDoesNotMatch']]);
209 | }
210 |
211 | my $json;
212 | try {
213 | $json = JSON::XS->new->relaxed->decode(decode_base64($policy));
214 | } catch {
215 | return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']]);
216 | };
217 |
218 | return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument', 'Message' => 'No expiration time specified in policy document']]) unless (defined $json->{expiration});
219 |
220 | my $expiration = Date::Parse::str2time($json->{expiration});
221 |
222 | if ($self->{request}->{starttime} > $expiration) {
223 | return respondXML(400, ['Error' => ['Code' => 'ExpiredToken']]);
224 | }
225 | # $self->log("Expires:".$expiration."; Starttime:".$self->{request}->{starttime});
226 |
227 | foreach my $ref (@{$json->{conditions}}) {
228 | if (ref $ref eq 'HASH') {
229 | foreach my $key (keys %{$ref}) {
230 | my $val = encode("utf8", $$ref{$key}); #TODO: better to decode parameter? Is unicode normalization required?
231 | my $result = $self->check_policy('eq', $key, $val);
232 | # $self->log($key."=".$val."(".$result.")");
233 | unless ($result) {return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']])};
234 | }
235 | }
236 | if (ref $ref eq 'ARRAY') {
237 | my $key = $$ref[1];
238 | $key =~ s/^\$//;
239 | my $val = encode("utf8", $$ref[2]); #TODO: better to decode parameter? Is unicode normalization required?
240 | my $result = $self->check_policy($$ref[0], $key, $val);
241 | # $self->log($key." ".$$ref[0]." ".$val." (".$result.")");
242 | unless ($result) {return respondXML(400, ['Error' => ['Code' => 'InvalidPolicyDocument']])};
243 | }
244 | }
245 |
246 | my $data = $req->parameters->get('file');
247 | unless ($data) {
248 | return respondXML(400, ['Error' => ['Code' => 'IncorrectNumberOfFilesInPostRequest']]);
249 | }
250 |
251 | my $md5 = md5_base64($data);
252 | unless ($req->parameters->get('Content-MD5') eq $md5.'==') {
253 | return respondXML(400, ['Error' => ['Code' => 'BadDigest']])
254 | }
255 |
256 | my $key = $req->parameters->get('key');
257 | my $store = $self->{buckets}->{$request->{bucket}}->{store};
258 |
259 | my $meta = {
260 | 'md5' => unpack('H*', decode_base64($md5)),
261 | 'acl' => $self->{req}->parameters->get('acl') || 'private'
262 | };
263 |
264 | $store->store_file($key, $req->parameters->get('file'), JSON::XS->new->utf8->encode($meta));
265 |
266 | my $status = $req->parameters->get('success_action_status');
267 | $status = '403' unless (($status eq '200') || ($status eq '201'));
268 |
269 | # TODO: access_action_redirect
270 |
271 | return respond($status, '');
272 | }
273 |
274 | sub handle_HEAD {
275 | my ($self) = @_;
276 |
277 | my $request = $self->{request};
278 | my $env = $request->{env};
279 | my $key = $request->{key};
280 |
281 | my $store = $self->{buckets}->{$request->{bucket}}->{store};
282 |
283 | unless ($store->check_exists($key)) {
284 | return respondXML(404, ['Error' => ['Code' => 'NoSuchKey']]);
285 | }
286 |
287 | my $meta;
288 | try {
289 | $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key));
290 | };
291 | unless (ref($meta) eq 'HASH') {
292 | $meta = {};
293 | }
294 |
295 | my $headers = ['Content-Length' => $store->get_size($key)];
296 | if ($meta->{type}) {
297 | push @$headers, 'Content-Type';
298 | push @$headers, $meta->{type};
299 | }
300 | if ($meta->{md5}) {
301 | push @$headers, 'ETag';
302 | push @$headers, "\"".$meta->{md5}."\"";
303 | }
304 |
305 | return [200, $headers, []];
306 | }
307 |
308 | sub handle_GET {
309 | my ($self) = @_;
310 |
311 | my $request = $self->{request};
312 | my $env = $request->{env};
313 |
314 | my $key = $request->{key};
315 |
316 | my $store = $self->{buckets}->{$request->{bucket}}->{store};
317 |
318 | unless($store->check_exists($key)){
319 | return respondXML(404, ['Error' => ['Code' => 'NoSuchKey']]);
320 | }
321 |
322 | my $meta;
323 | try {
324 | $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key));
325 | };
326 | unless (ref($meta) eq 'HASH') {
327 | $meta = {};
328 | }
329 |
330 | my $headers = ['Content-Length' => $store->get_size($key)];
331 | my $ct = $request->{uri}->query_param('response-content-type');
332 | $ct = $meta->{type} unless ($ct);
333 | if ($ct) {
334 | push @$headers, 'Content-Type';
335 | push @$headers, $ct;
336 | }
337 | if ($meta->{md5}) {
338 | push @$headers, 'ETag';
339 | push @$headers, "\"".$meta->{md5}."\"";
340 | }
341 |
342 | return [200, $headers, $store->retrieve_file($key)];
343 | }
344 |
345 |
346 | sub handle_PUT {
347 | my $self = shift;
348 |
349 | my $request = $self->{request};
350 | my $env = $request->{env};
351 | my $store = $self->{buckets}->{$request->{bucket}}->{store};
352 |
353 | my $key = $request->{key};
354 |
355 | my $cl = $env->{CONTENT_LENGTH};
356 | my $source = $env->{HTTP_X_AMZ_COPY_SOURCE};
357 |
358 | if (($cl == 0) && ($source)) {
359 | # Copy File
360 | $source = uri_unescape($source);
361 | (my $sourceBucket, my $sourceKey) = $source =~ m/^\/([^\?\/]*)\/?([^\?]*)/;
362 |
363 | # $self->log("Source: ".$sourceBucket."/bla/".$sourceKey."\nDestinationKey: ".$key."\n");
364 |
365 | my $res = $store->link_files($sourceKey, $key);
366 |
367 | if ($res) {
368 |
369 | my $meta;
370 | try {
371 | $meta = JSON::XS->new->utf8->decode($store->retrieve_filemeta($key));
372 | };
373 | unless (ref($meta) eq 'HASH') {
374 | $meta = {};
375 | }
376 |
377 | return respondXML(200, ['CopyObjectResult' => [ 'LastModified' => '2012', 'ETag' => $meta->{md5}]]);
378 | } else {
379 | return respondXML(500, ['Error' => ['Code' => 'InternalError']]);
380 | }
381 | } else {
382 | # Normal PUT
383 | my $input = $env->{'psgi.input'};
384 | my $cl = $env->{CONTENT_LENGTH};
385 | my $data;
386 |
387 | if (($input->read($data, $cl)) != $cl) {
388 | return respondXML(400, ['Error' => ['Code' => 'IncompleteBody']]);
389 | }
390 |
391 | my $md5 = md5_base64($data);
392 |
393 | my $meta = {};
394 | $meta->{type} = $env->{CONTENT_TYPE} if ($env->{CONTENT_TYPE});
395 | $meta->{acl} = $env->{HTTP_X_AMZ_ACL} || 'private';
396 | $meta->{md5} = unpack('H*', decode_base64($md5));
397 |
398 | if ($env->{HTTP_CONTENT_MD5}) {
399 | return respondXML(400, ['Error' => ['Code' => 'BadDigest']]) unless ($env->{HTTP_CONTENT_MD5} eq $md5.'==');
400 | }
401 |
402 | $store->store_file($key, $data, JSON::XS->new->utf8->encode($meta));
403 |
404 | return respond(200, '');
405 | }
406 | }
407 |
408 | sub handle_DELETE {
409 | my $self = shift;
410 |
411 | my $request = $self->{request};
412 | my $env = $request->{env};
413 | my $store = $self->{buckets}->{$request->{bucket}}->{store};
414 |
415 |
416 | my $key = $request->{key};
417 |
418 | unless ($store->check_exists($key)) {
419 | return respondXML(404, ['Error' => ['Code' => 'NoSuchKey', 'Message' => 'The resource you requested does not exist', 'Resource' => $key]]);
420 | }
421 |
422 | if ($store->delete_file($key)) {
423 | return [204, [], []];
424 | } else {
425 | return respondXML(500, ['Error' => ['Code' => 'InternalError']]);
426 | }
427 |
428 | }
429 |
430 | sub request_uri {
431 | my $env = shift;
432 |
433 | my $uri = ($env->{'psgi.url_scheme'} || "http") .
434 | "://" .
435 | ($env->{HTTP_HOST} || (($env->{SERVER_NAME} || "") . ":" . ($env->{SERVER_PORT} || 80))) .
436 | ($env->{SCRIPT_NAME} || "");
437 |
438 | return URI->new($uri . $env->{REQUEST_URI})->canonical();
439 | }
440 |
441 | sub handle {
442 | my ($self, $env) = @_;
443 |
444 | my $request = {};
445 |
446 | $request->{env} = $env;
447 | $request->{starttime} = time();
448 |
449 | $request->{uri} = request_uri($env);
450 |
451 | # split in bucket and key (currently only path style buckets no host style)
452 | ($request->{bucket}, $request->{key_escaped}) = $env->{REQUEST_URI} =~ m/^\/([^\?\/]*)\/?([^\?]*)/;
453 | $request->{key} = uri_unescape($request->{key_escaped}) || '';
454 |
455 | return respond(200, "Nothing to see here") if ($request->{bucket} eq '');
456 |
457 | if (not defined $self->{buckets}->{$request->{bucket}}) {
458 | return respondXML(404,
459 | ['Error' =>
460 | ['Code' => 'NoSuchBucket',
461 | 'Message' => 'The specified bucket does not exist',
462 | 'BucketName' => $request->{bucket}]
463 | ]);
464 | }
465 |
466 | $self->{request} = $request;
467 |
468 | # TODO: body parsing for POST. Parameter "file" should be saved as file instead of in memory
469 | my $req = Plack::Request->new($env);
470 | $self->{req} = $req;
471 |
472 | my @methods = qw(POST GET HEAD PUT DELETE);
473 |
474 | unless ($env->{REQUEST_METHOD} ~~ @methods) {
475 | undef($self->{request});
476 |
477 | return respondXML(405,
478 | ['Error' =>
479 | ['Code' => 'MethodNotAllowed',
480 | 'Message' => 'The specified method is not allowed']
481 | ]);
482 | }
483 |
484 | my $result;
485 | if ($env->{REQUEST_METHOD} eq 'POST') {
486 | $result = $self->handle_POST();
487 | } else {
488 |
489 | unless ($self->check_signature()) {
490 | undef($self->{request});
491 | return respondXML(403, ['Error' => ['Code' => 'SignatureDoesNotMatch']]);
492 | }
493 |
494 | my $method = 'handle_'.$env->{REQUEST_METHOD};
495 | $result = $self->$method;
496 | }
497 |
498 | undef($self->{request});
499 |
500 | return $result;
501 | };
502 |
503 |
504 | sub psgi_callback {
505 | my $self = shift;
506 |
507 | sub {
508 | $self->handle( shift );
509 | };
510 | }
511 |
512 | 1;
513 |
--------------------------------------------------------------------------------