├── 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 | 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 | $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 | '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 .= ''; 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 | --------------------------------------------------------------------------------