├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── photobackup_bottle ├── __init__.py ├── init.py └── photobackup.py ├── setup.py └── tests └── test_api.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | fixme: 8 | enabled: true 9 | radon: 10 | enabled: true 11 | ratings: 12 | paths: 13 | - "**.py" 14 | exclude_paths: [] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | photobackup_settings.py 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # - "3.2" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | # command to install dependencies 9 | install: 10 | - pip install requests pytest 11 | - python setup.py install 12 | script: 13 | - rm -f /home/travis/.photobackup 14 | - printf '[photobackup]\nBindAddress = 127.0.0.1\nMediaRoot = /tmp\nPort = 8420\nPassword = 2c1ee68372215b1ce064426b5cdbd4ef2581ace0dd3b21fa2be27f364827242e83f68b68be03f5b3e24be5d1b4315f98a0a96d19713fb3a19dc455fb6adc3431\nPasswordBcrypt = $2b$12$ghLyUVMOgbZ7bTOVqMIs0ePb0rDJNtvpKpF4Pb7JFSOHMBWtSbI2G\n' > /home/travis/.photobackup 15 | - photobackup run & sleep 2 && py.test tests 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include photobackup_bottle/index.html 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Python Version](https://img.shields.io/badge/Python-3-brightgreen.svg?style=plastic)](http://python.org) 2 | [![PyPI version](https://badge.fury.io/py/photobackup-bottle.svg)](https://badge.fury.io/py/photobackup-bottle) 3 | [![Build Status](https://travis-ci.org/PhotoBackup/server-bottle.svg?branch=master)](https://travis-ci.org/PhotoBackup/server-bottle) 4 | [![Code Issues](https://www.quantifiedcode.com/api/v1/project/0066628ce3954e079603dfeafdf5b077/badge.svg)](https://www.quantifiedcode.com/app/project/0066628ce3954e079603dfeafdf5b077) 5 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/PhotoBackup/server-bottle/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 6 | 7 | Join our online chat at [![Gitter](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/PhotoBackup) 8 | 9 | # The Python PhotoBackup server implementation 10 | 11 | The Python3 implementation of PhotoBackup server, made with 12 | [bottle](http://bottlepy.org/). It follows the 13 | [official API](https://github.com/PhotoBackup/api/blob/master/api.raml), currently in [version 2](https://github.com/PhotoBackup/api/releases/tag/v2). 14 | 15 | ## Requirements 16 | 17 | You need: 18 | 19 | - [Python3](https://www.python.org/) ; 20 | - [pip](https://pip.pypa.io/en/stable/) ; 21 | - libffi-dev (installable though `[apt|yum] install libffi-dev`) 22 | 23 | ## Installation 24 | 25 | Install through [PyPI](https://pypi.python.org/pypi): 26 | 27 | pip install photobackup_bottle 28 | 29 | Then run the installer, which asks for the directory to save your pictures to 30 | and the server password: 31 | 32 | photobackup init 33 | 34 | The script looks for the directory to be writable by the usual `www-data` user. 35 | It fails gracefully if it is not, just warning you to make it work properly. 36 | This step creates a `.photobackup` file in the user's home directory, 37 | containing: 38 | 39 | * `BindAddress`, the IP address (default is `127.0.0.1`) ; 40 | * `MediaRoot`, the directory where the pictures are written in ; 41 | * `Password`, the SHA-512 hashed password ; 42 | * `PasswordBcrypt`, a Bcrypt-ed version of your SHA-512 hashed password ; 43 | * `Port`, the port (default is `8420`). 44 | 45 | ## Usage 46 | 47 | Launch the server with: 48 | 49 | photobackup run 50 | 51 | By default, it runs on host `127.0.0.1`, port `8420` and reloads automatically. 52 | 53 | ## Production 54 | 55 | To put in production, use [Nginx](http://nginx.org/) to bind a sever name to `http://127.0.0.1:8420`. 56 | -------------------------------------------------------------------------------- /photobackup_bottle/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2013-2016 Stéphane Péchard. 3 | # 4 | # This file is part of PhotoBackup. 5 | # 6 | # PhotoBackup is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # PhotoBackup is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """ Some PhotoBackup Python server configuration. 20 | """ 21 | 22 | __version__ = "0.1.3" 23 | -------------------------------------------------------------------------------- /photobackup_bottle/init.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2013-2016 Stéphane Péchard. 3 | # 4 | # This file is part of PhotoBackup. 5 | # 6 | # PhotoBackup is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # PhotoBackup is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """ PhotoBackup Python server initialization module. 20 | 21 | It asks the user for configuration and writes it 22 | to a .ini file. 23 | """ 24 | 25 | # stlib 26 | import configparser 27 | import getpass 28 | import hashlib 29 | import os 30 | import pwd 31 | import stat 32 | # pipped 33 | import bcrypt 34 | 35 | 36 | def writable_by(dirname, name, user_or_group): 37 | """ Checks if the given directory is writable by the named user or group. 38 | user_or_group is a boolean with True for a user and False for a group. """ 39 | try: 40 | pwnam = pwd.getpwnam(name) 41 | except KeyError: 42 | print('[ERROR] User or group {0} does not exist!'.format(name)) 43 | return False 44 | ugid = pwnam.pw_uid if user_or_group else pwnam.pw_gid 45 | 46 | dir_stat = os.stat(dirname) 47 | ug_stat = dir_stat[stat.ST_UID] if user_or_group else dir_stat[stat.ST_GID] 48 | iw_stat = stat.S_IWUSR if user_or_group else stat.S_IWGRP 49 | 50 | if ((ug_stat == ugid) and (dir_stat[stat.ST_MODE] & iw_stat)): 51 | return True 52 | 53 | return False 54 | 55 | 56 | def init(username=None): 57 | """ Initializes the PhotoBackup configuration file. """ 58 | print("""=============================== 59 | PhotoBackup_bottle init process 60 | ===============================""") 61 | 62 | # ask for the upload directory (should be writable by the server) 63 | media_root = input("The directory where to put the pictures" + 64 | " (should be writable by the server you use): ") 65 | try: 66 | os.mkdir(media_root) 67 | print("Directory {0} does not exist, creating it".format(media_root)) 68 | except OSError: 69 | print("Directory already exists") 70 | 71 | # test for user writability of the directory 72 | server_user = input("Owner of the directory [www-data]: ") 73 | if not server_user: 74 | server_user = 'www-data' 75 | if not writable_by(media_root, server_user, True) and \ 76 | not writable_by(media_root, server_user, False): 77 | print('[INFO] Directory {0} is not writable by {1}, check it!' 78 | .format(media_root, server_user)) 79 | 80 | # ask a password for the server 81 | password = getpass.getpass(prompt='The server password: ') 82 | pass_sha = hashlib.sha512( 83 | password.encode('utf-8')).hexdigest().encode('utf-8') 84 | passhash = bcrypt.hashpw(pass_sha, bcrypt.gensalt()) 85 | 86 | # save in config file 87 | config_file = os.path.expanduser("~/.photobackup") 88 | config = configparser.ConfigParser() 89 | config.optionxform = str # to keep case of keys 90 | config.read(config_file) # to keep existing data 91 | suffix = '-' + username if username else '' 92 | config_key = 'photobackup' + suffix 93 | config[config_key] = {'BindAddress': '127.0.0.1', 94 | 'MediaRoot': media_root, 95 | 'Password': pass_sha.decode(), 96 | 'PasswordBcrypt': passhash.decode(), 97 | 'Port': 8420} 98 | with open(config_file, 'w') as configfile: 99 | config.write(configfile) 100 | 101 | 102 | if __name__ == '__main__': 103 | init() 104 | -------------------------------------------------------------------------------- /photobackup_bottle/photobackup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (C) 2013-2016 Stéphane Péchard. 3 | # 4 | # This file is part of PhotoBackup. 5 | # 6 | # PhotoBackup is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # PhotoBackup is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | """PhotoBackup Python server. 20 | 21 | Usage: 22 | photobackup init [] 23 | photobackup run [] 24 | photobackup list 25 | photobackup (-h | --help) 26 | photobackup --version 27 | 28 | Options: 29 | -h --help Show this screen. 30 | --version Show version. 31 | """ 32 | 33 | # stlib 34 | import configparser 35 | import json 36 | import os 37 | import sys 38 | # pipped 39 | import bcrypt 40 | from bottle import abort, redirect, request, response, route, run 41 | import bottle 42 | from docopt import docopt 43 | from logbook import info, warn, error, Logger, StreamHandler 44 | # local 45 | from . import __version__, init 46 | 47 | 48 | def create_logger(): 49 | """ Creates the logger fpr this module. """ 50 | StreamHandler(sys.stdout).push_application() 51 | return Logger('PhotoBackup') 52 | 53 | 54 | def init_config(username=None): 55 | """ Launch init.py script to create configuration file on user's disk. """ 56 | init.init(username) 57 | sys.exit("\nCreated, now launch PhotoBackup server with 'photobackup run'") 58 | 59 | 60 | def print_list(): 61 | """ Print the existing PhotoBackup configurations. """ 62 | sections = '\n'.join(get_config().sections()) 63 | sections = sections.replace('photobackup-', '- ') 64 | sections = sections.replace('photobackup', '') 65 | print('Runnable PhotoBackup configurations are:') 66 | print(sections) 67 | 68 | 69 | def read_config(username=None): 70 | """ Set configuration file data into local dictionnary. """ 71 | config_file = os.path.expanduser("~/.photobackup") 72 | config = configparser.RawConfigParser() 73 | config.optionxform = lambda option: option # to keep case of keys 74 | try: 75 | config.read_file(open(config_file)) 76 | except EnvironmentError: 77 | log.error("can't read configuration file, running 'photobackup init'") 78 | init_config(username) 79 | 80 | suffix = '-' + username if username else '' 81 | config_key = 'photobackup' + suffix 82 | 83 | values = None 84 | try: 85 | values = config[config_key] 86 | except KeyError: 87 | values = None 88 | return values 89 | 90 | 91 | def end(code, message): 92 | """ Aborts the request and returns the given error. """ 93 | log.error(message) 94 | response.status = code 95 | response.content_type = 'application/json' 96 | return json.dumps({'error': message}) 97 | 98 | 99 | def validate_password(request, isTest=False): 100 | """ Validates the password given in the request 101 | against the stored Bcrypted one. """ 102 | password = None 103 | try: 104 | password = request.forms.get('password').encode('utf-8') 105 | except AttributeError: 106 | return end(403, "No password in request") 107 | 108 | if 'PasswordBcrypt' in config: 109 | passcrypt = config['PasswordBcrypt'].encode('utf-8') 110 | if bcrypt.hashpw(password, passcrypt) != passcrypt: 111 | return end(403, "wrong password!") 112 | elif 'Password' in config and config['Password'] != password: 113 | return end(403, "wrong password!") 114 | elif isTest: 115 | return end(401, "There's no password in server configuration!") 116 | 117 | 118 | def save_file(upfile, filesize): 119 | """ Saves the sent file locally. """ 120 | path = os.path.join(config['MediaRoot'], os.path.basename(upfile.raw_filename)) 121 | if not os.path.exists(path): 122 | 123 | # save file 124 | log.info("upfile path: " + path) 125 | upfile.save(config['MediaRoot']) 126 | 127 | # check file size in request against written file size 128 | if filesize != os.stat(path).st_size: 129 | return end(411, "file sizes do not match!") 130 | 131 | elif filesize == os.stat(path).st_size: 132 | return end(409, "file exists and is complete") 133 | 134 | else: 135 | log.warn("file " + path + " is incomplete, resaving!") 136 | try: 137 | os.remove(path) 138 | except OSError: 139 | log.info("File already removed, strange but cool...") 140 | try: 141 | upfile.save(config['MediaRoot']) 142 | except OSError: 143 | log.error("Impossible to save the file...") 144 | 145 | 146 | def get_files(): 147 | """ List all locally saved files. """ 148 | return [entry.name for entry in os.scandir(config['MediaRoot']) 149 | if not entry.name.startswith('.') and entry.is_file()] 150 | 151 | 152 | # Bottle routes 153 | @route('/') 154 | def index(): 155 | """ Redirects to the PhotoBackup website. """ 156 | redirect("https://photobackup.github.io/") 157 | 158 | 159 | @route('/', method='POST') 160 | def save_image(): 161 | """ Saves the given image to the parameterized directory. """ 162 | answer = validate_password(request) 163 | if answer: 164 | return answer 165 | 166 | upfile = request.files.get('upfile') 167 | if not upfile: 168 | return end(401, "no file in the request!") 169 | 170 | filesize = -1 171 | try: 172 | filesize = int(request.forms.get('filesize')) 173 | return save_file(upfile, filesize) 174 | except TypeError: 175 | return end(400, "Missing file size in the request!") 176 | 177 | 178 | @route('/test', method='POST') 179 | def test(): 180 | """ Tests the server capabilities to handle POST requests. """ 181 | answer = validate_password(request, True) 182 | if answer: 183 | return answer 184 | 185 | if not os.path.exists(config['MediaRoot']): 186 | return end(500, "'MediaRoot' directory does not exist!") 187 | 188 | testfile = os.path.join(config['MediaRoot'], '.test_file_to_write') 189 | try: 190 | with open(testfile, 'w') as tf: 191 | tf.write('') 192 | log.info("Test succeeded \o/") 193 | return {'uploaded_files': get_files() } 194 | except EnvironmentError: 195 | return end(500, "Can't write to 'MediaRoot' directory!") 196 | finally: 197 | os.remove(testfile) 198 | 199 | 200 | # variables 201 | arguments = docopt(__doc__, version='PhotoBackup ' + __version__) 202 | log = create_logger() 203 | config = read_config(arguments['']) 204 | 205 | 206 | def main(): 207 | """ Prepares and launches the bottle app. """ 208 | if (arguments['init']): 209 | init_config(arguments['']) 210 | elif (arguments['run']): 211 | app = bottle.default_app() 212 | if 'HTTPPrefix' in config: 213 | app.mount(config['HTTPPrefix'], app) 214 | app.run(port=config['Port'], host=config['BindAddress']) 215 | elif (arguments['list']): 216 | print_list() 217 | 218 | 219 | if __name__ == '__main__': 220 | main() 221 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup module for photobackup-bottle. 2 | 3 | See: https://photobackup.github.io/ 4 | """ 5 | 6 | # system 7 | from setuptools import setup, find_packages 8 | # local 9 | from photobackup_bottle import __version__ 10 | 11 | 12 | setup( 13 | name='photobackup_bottle', 14 | version=__version__, 15 | description='The simplest PhotoBackup server, made with bottle', 16 | long_description=open('README.md').read(), 17 | url='https://photobackup.github.io/', 18 | author='s13d', 19 | author_email='photobackup@s13d.fr', 20 | keywords='pictures photographs mobile backup', 21 | packages=find_packages(), 22 | include_package_data=True, 23 | install_requires=['bcrypt', 'bottle', 'docopt', 'logbook'], 24 | 25 | classifiers=[ 26 | 'Development Status :: 3 - Alpha', 27 | 'Framework :: Bottle', 28 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 29 | 'Programming Language :: Python :: 3.2', 30 | 'Programming Language :: Python :: 3.3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Natural Language :: English', 33 | 'Topic :: System :: Archiving :: Backup', 34 | ], 35 | 36 | entry_points={ 37 | 'console_scripts': [ 38 | 'photobackup=photobackup_bottle.photobackup:main', 39 | ], 40 | }, 41 | ) 42 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | # stlib 2 | import configparser 3 | import os 4 | # pipped 5 | import pytest 6 | import requests 7 | 8 | 9 | # read server config file 10 | filename = os.path.expanduser("~/.photobackup") 11 | parser = configparser.ConfigParser() 12 | parser.read_file(open(filename)) 13 | config = parser['photobackup'] 14 | 15 | # stuff we need 16 | prefix = '' 17 | upfile_name = 'test_api.py' 18 | upfile = os.path.join('tests', upfile_name) 19 | url = 'http://' + config['BindAddress'] + ':' + config['Port'] 20 | try: 21 | prefix = config['HTTPPrefix'] 22 | except KeyError: 23 | print("No HTTPPrefix in config") 24 | finally: 25 | url += prefix 26 | print("URL = " + url) 27 | 28 | 29 | ######### 30 | # Tests # 31 | ######### 32 | 33 | def remove_sent_file(): 34 | file_to_remove = os.path.join(config['MediaRoot'], upfile_name) 35 | try: 36 | os.remove(file_to_remove) 37 | print('File ' + file_to_remove + ' has been removed') 38 | except OSError: # should be FileNotFoundError for Python > 3.2 39 | print("File does not exist, no need to remove it.") 40 | 41 | 42 | class TestClass: 43 | 44 | def setup_method(self, method): 45 | """ clean before each test """ 46 | remove_sent_file() 47 | 48 | def teardown_method(self, method): 49 | """ clean after each test """ 50 | remove_sent_file() 51 | 52 | def test_testendpoint200(self): 53 | """ Test the status when posting a test. """ 54 | payload = {'password': config['Password']} 55 | r = requests.post(url + '/test', data=payload) 56 | assert r.status_code == 200 57 | 58 | def test_rootendpoint200(self): 59 | """ Test the simplest route of the server. """ 60 | r = requests.get(url) 61 | assert r.status_code == 200 62 | 63 | def test_nopwd403(self): 64 | """ Test the status when posting with no argument. """ 65 | r = requests.post(url) 66 | assert r.status_code == 403 67 | 68 | def test_wrongpwd403(self): 69 | """ Test the status when posting with a wrong password. """ 70 | payload = {'password': 'WRONG PASSWORD'} 71 | r = requests.post(url, data=payload) 72 | assert r.status_code == 403 73 | 74 | def test_noupfile401(self): 75 | """ Test the status when posting with right password but no upfile. """ 76 | payload = {'password': config['Password']} 77 | r = requests.post(url, data=payload) 78 | assert r.status_code == 401 79 | 80 | def test_nofilesize400(self): 81 | """ Test the status when posting with the right password, an upfile 82 | but no file size parameter. """ 83 | payload = {'password': config['Password']} 84 | r = requests.post(url, data=payload, 85 | files={'upfile': open(upfile, 'rb')}) 86 | assert r.status_code == 400 87 | 88 | def test_sendfile200(self): 89 | """ Test the status when posting with all the right parameters. """ 90 | payload = { 91 | 'password': config['Password'], 92 | 'filesize': os.stat(upfile).st_size 93 | } 94 | r = requests.post(url, data=payload, 95 | files={'upfile': open(upfile, 'rb')}) 96 | assert r.status_code == 200 97 | 98 | def test_resendfile409(self): 99 | """ Test the status when reposting with all the right parameters. """ 100 | payload = { 101 | 'password': config['Password'], 102 | 'filesize': os.stat(upfile).st_size 103 | } 104 | r = requests.post(url, data=payload, 105 | files={'upfile': open(upfile, 'rb')}) 106 | r = requests.post(url, data=payload, 107 | files={'upfile': open(upfile, 'rb')}) 108 | assert r.status_code == 409 109 | 110 | # def test_dashboard200(self): 111 | # """ Test the simplest route of the server. """ 112 | # r = requests.get(url + '/dashboard') 113 | # assert r.status_code == 200 114 | --------------------------------------------------------------------------------