Compare commits

...

21 Commits

Author SHA1 Message Date
BBaoVanC 70e84ec8f0
Remove unused flask Response import 2020-12-16 15:25:50 -06:00
BBaoVanC b11c7c2845
Strip EXIF from image before saving 2020-11-24 17:51:37 -06:00
BBaoVanC 86ec88db08
Update .gitignore 2020-11-24 17:37:02 -06:00
BBaoVanC fb416d1c0d
Fix installation instructions 2020-11-20 11:43:35 -06:00
BBaoVanC b20b78fd17
Fix syntax error at imgupload.py:109 2020-11-20 11:38:26 -06:00
BBaoVanC 2b8330e32a
Clean up README.md 2020-10-04 11:15:34 -05:00
BBaoVanC 63818bd371
Change usage of str.format() to f-strings 2020-10-04 11:13:59 -05:00
BBaoVanC 67cb916ac9
Add git checkout step to uWSGI guide 2020-09-26 23:24:59 -05:00
BBaoVanC 9910bc279c
Add imageName field to request a specific name to save to 2020-09-15 17:24:36 -05:00
BBaoVanC f100732e4d
Check if image with same name already exists 2020-09-15 17:20:41 -05:00
BBaoVanC 46420eecda
Remove leftover debug print statements in configtest.py 2020-09-15 16:35:50 -05:00
BBaoVanC 7b372a7b6f
Increase processes in uwsgi.ini.default 2020-09-12 19:48:42 -05:00
BBaoVanC 5570710432
Rewrite installation section in README.md
Also added uWSGI installation tutorial
2020-09-08 00:39:48 -05:00
BBaoVanC ba68674e4e
Add GitHub-specific README.md which links Gitea 2020-09-07 20:31:24 -05:00
BBaoVanC 99ff2c68a3
Change references of gitea.bbaovanc.com to git.bbaovanc.com 2020-09-07 02:07:36 -05:00
BBaoVanC dd069bf395
Add newline after block comments at beginning 2020-09-06 15:24:20 -05:00
BBaoVanC f21adfa04e
Added FAQ and links to sections in README.md 2020-09-06 12:43:44 -05:00
BBaoVanC 3d5c55498f
Clean up and add LICENSE section to README.md 2020-09-06 11:26:13 -05:00
BBaoVanC 5e2be10434
Change references of GitHub to gitea.bbaovanc.com 2020-09-06 11:09:06 -05:00
BBaoVanC 7c1f449bce
Add "verify" field to request to not save image
This makes it easy for the user to debug authentication.
2020-09-05 18:55:56 -05:00
BBaoVanC 0dbcc0e380
Change file extension check to be case-insensitive 2020-09-05 16:21:50 -05:00
9 changed files with 183 additions and 51 deletions

7
.github/README.md vendored Normal file
View File

@ -0,0 +1,7 @@
# imgupload
## Moving from GitHub to Gitea
**TL;DR: Please go to my Gitea instance instead of GitHub for anything related to imgupload. [https://git.bbaovanc.com/bbaovanc/imgupload](https://git.bbaovanc.com/bbaovanc/imgupload)**
This repository might not exist on GitHub in the future! Releases will not be released here in the future. Instead, they will be released on the repository on my Gitea instance, which you can find [here](https://git.bbaovanc.com/bbaovanc/imgupload). Issues and pull requests should also be created on Gitea. For now, commits will still be pushed to this repository, but that may change in the future.

27
.gitignore vendored
View File

@ -1,3 +1,8 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -50,6 +55,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
@ -70,6 +76,7 @@ instance/
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
@ -109,6 +116,7 @@ venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
@ -128,6 +136,25 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### VisualStudioCode ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python
# imgupload custom
uploadkeys
savelog.log

View File

@ -1,26 +1,88 @@
# imgupload
![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/BBaoVanC/imgupload/master?color=purple) ![GitHub repo size](https://img.shields.io/github/repo-size/bbaovanc/imgupload?color=purple) ![GitHub All Releases](https://img.shields.io/github/downloads/bbaovanc/imgupload/total?color=purple) ![GitHub issues](https://img.shields.io/github/issues/bbaovanc/imgupload?color=purple) ![GitHub closed issues](https://img.shields.io/github/issues-closed/bbaovanc/imgupload?color=purple) ![GitHub](https://img.shields.io/github/license/bbaovanc/imgupload?color=purple)
### What is imgupload?
<!---![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/BBaoVanC/imgupload/master?color=purple)
![GitHub repo size](https://img.shields.io/github/repo-size/bbaovanc/imgupload?color=purple)
![GitHub All Releases](https://img.shields.io/github/downloads/bbaovanc/imgupload/total?color=purple)
![GitHub issues](https://img.shields.io/github/issues/bbaovanc/imgupload?color=purple)
![GitHub closed issues](https://img.shields.io/github/issues-closed/bbaovanc/imgupload?color=purple)
![GitHub](https://img.shields.io/github/license/bbaovanc/imgupload?color=purple)-->
## What is imgupload?
imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests.
### Installation
1. Clone the repository: `git clone https://github.com/BBaoVanC/imgupload.git`
2. Enter the imgupload directory: `cd imgupload`
3. Create a virtualenv: `python3 -m venv env`
4. Enter the virtualenv: `source env/bin/activate`
5. Install dependencies: `python3 -m pip install -r requirements.txt`
6. Run the Flask app
---
## FAQ
**Where can I send bug reports and feature requests?**
You can create an issue [here](https://git.bbaovanc.com/bbaovanc/imgupload/issues).
**How do I use this program?**
See [Installation](#installation)
**I want to make a pull request. Where should I do that?**
First, fork [this repository](https://git.bbaovanc.com/bbaovanc/imgupload). If you don't have an account on my Gitea site yet, you can either create one, or sign in using your GitHub account. Commit your changes to your fork, and then create a pull request.
---
## Installation
## Running the Flask app
### Using uWSGI
[https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html](https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html)
Instructions specific to imgupload are coming soon
Note: replace `www-data` with whatever user your webserver runs as.
1. Go to /srv: `cd /srv`
2. Clone the repository: `git clone https://git.bbaovanc.com/bbaovanc/imgupload.git`
3. Change ownership of /srv/imgupload: `sudo chown www-data:www-data /srv/imgupload`
4. Enter www-data user: `sudo su www-data`
5. Change directories to /srv/imgupload: `cd /srv/imgupload`
6. Checkout the version you want (replace [version] with desired version tag: `git checkout [version]`
7. Enter the imgupload directory: `cd imgupload`
8. Create a virtualenv: `python3 -m venv env`
9. Enter the virtualenv: `source env/bin/activate`
10. Install dependencies: `python3 -m pip install -r requirements.txt`
11. Leave the www-data user: `exit`
12. Copy the default uWSGI configuration: `sudo cp /srv/imgupload/uwsgi.ini.default /etc/uwsgi/apps-available/imgupload.ini`
13. Modify `/etc/uwsgi/apps-available/imgupload.ini` to your preferences
14. Enable imgupload: `sudo ln -s /etc/uwsgi/apps-available/imgupload.ini /etc/uwsgi/apps-enabled/`
15. Restart uWSGI: `sudo systemctl restart uwsgi`
16. Set up your webserver to proxy the uwsgi.sock
Example NGINX location block:
```nginx
location /upload {
include uwsgi_params;
uwsgi_pass unix:/srv/imgupload/uwsgi.sock;
client_max_body_size 25M;
}
```
### Using Flask development server
#### Setup
```shell
$ source env/bin/activate # if you haven't already entered the virtualenv
$ export FLASK_APP=imgupload.py
$ flask run
git clone https://git.bbaovanc.com/bbaovanc/imgupload.git
cd imgupload
python3 -m venv env
source env/bin/activate
pip3 install -r requirements.txt
```
#### Run
```shell
export FLASK_APP=imgupload.py
flask run
```
---
## License
_imgupload_ is licensed under the GPLv3 license. For more information, please refer to [`LICENSE`](https://git.bbaovanc.com/bbaovanc/imgupload/src/branch/master/LICENSE)

View File

@ -4,6 +4,7 @@ configtest.py
Tests the validity of your configuration in settings.py.
"""
import os
import settings as settings
@ -34,7 +35,7 @@ unset_settings = [i for i in defaults.keys() if i not in dir(settings)]
if len(unset_settings) > 0:
for unset in unset_settings:
checksettings.remove(unset)
print("[!] {0} is unset. The default value is type {1} with value {2}".format(unset, deftypes[unset].__name__, defaults[unset]))
print(f"[!] {unset} is unset. The default value is type {deftypes[unset].__name__} with value {defaults[unset]}")
else:
print("[" + u"\u2713" + "] Found all required settings!")
@ -44,7 +45,7 @@ typesgood = True
typeswrong = []
for testtype in checksettings:
if type(getattr(settings, testtype)) is not deftypes[testtype]:
print("[!] {0} requires {1}, but is {2}".format(testtype, deftypes[testtype].__name__, type(getattr(settings, testtype)).__name__))
print(f"[!] {testtype} requires {deftypes[testtype].__name__}, but is {type(getattr(settings, testtype)).__name__}")
typeswrong.append(testtype)
typesgood = False
@ -62,7 +63,7 @@ if "ALLOWED_EXTENSIONS" in checksettings:
if len(invalid_exts) > 0:
print("[!] The following extensions listed in ALLOWED_EXTENSIONS are invalid:")
for e in invalid_exts:
print(" {0} is listed in ALLOWED_EXTENSIONS, but doesn't start with a .".format(e))
print(f" {e} is listed in ALLOWED_EXTENSIONS, but doesn't start with a .")
else:
print("[" + u"\u2713" + "] ALLOWED_EXTENSIONS is good!")
@ -72,7 +73,7 @@ uploadfolder_exists = True
if "UPLOAD_FOLDER" in checksettings:
if not os.path.isdir(settings.UPLOAD_FOLDER):
uploadfolder_exists = False
print("[!] The directory set in UPLOAD_FOLDER ('{0}') doesn't exist!".format(settings.UPLOAD_FOLDER))
print(f"[!] The directory set in UPLOAD_FOLDER ('{settings.UPLOAD_FOLDER}') doesn't exist!")
else:
print("[" + u"\u2713" + "] UPLOAD_FOLDER exists!")
@ -84,8 +85,6 @@ if "ROOTURL" in checksettings:
pass
else:
rooturl_good = False
print(settings.ROOTURL)
print(settings.ROOTURL.startswith("https://"))
print("[!] ROOTURL does not start with `http://` or `https://`! This may cause issues!")
if not settings.ROOTURL.endswith("/"):
rooturl_good = False
@ -93,14 +92,14 @@ if "ROOTURL" in checksettings:
if not rooturl_good:
print(" With your current settings, this is what a generated url would look like:")
print(" {0}example.png".format(settings.ROOTURL))
print(f" {settings.ROOTURL}example.png")
else:
print("[" + u"\u2713" + "] ROOTURL is good!")
# Ask the user if SAVELOG is the intended filename
if "SAVELOG" in checksettings:
print("[*] SAVELOG was interpreted to be {0}".format(settings.SAVELOG))
print(f"[*] SAVELOG was interpreted to be {settings.SAVELOG}")
print("[*] If this is not the intended filename, please fix it.")
@ -112,32 +111,32 @@ if len(unset_settings) > 0:
summarygood = False
print("Unset settings:")
for unset in unset_settings:
print(" {0}".format(unset))
print(f" {unset}")
if len(typeswrong) > 0:
summarygood = False
print("Incorrect types:")
for wtype in typeswrong:
print(" {0}".format(wtype))
print(f" {wtype}")
if len(invalid_exts) > 0:
summarygood = False
print("Invalid extensions:")
for wext in invalid_exts:
print(" '{0}'".format(wext))
print(f" '{wext}'")
if not uploadfolder_exists:
summarygood = False
print("UPLOAD_FOLDER ({0}) does not exist!".format(settings.UPLOAD_FOLDER))
print(f"UPLOAD_FOLDER ({settings.UPLOAD_FOLDER}) does not exist!")
if not rooturl_good:
summarygood = False
print("ROOTURL may cause issues!")
print("With current settings, this is what a generated URL would look like:")
print("{0}example.png".format(settings.ROOTURL))
print(f"{settings.ROOTURL}example.png")
if "SAVELOG" in checksettings:
print("[*] SAVELOG is {0}".format(settings.SAVELOG))
print(f"[*] SAVELOG is {settings.SAVELOG}")
if summarygood:
print("[" + u"\u2713" + "] This configuration passes all tests!")

View File

@ -4,6 +4,7 @@ functions.py
Functions used by imgupload which can be easily customized.
"""
import string
import random

View File

@ -4,11 +4,13 @@ imgupload.py
Flask application for processing images uploaded through POST requests.
"""
from flask import Flask, request, jsonify, Response
from flask import Flask, request, jsonify
from flask_api import status
from pathlib import Path
import os
import datetime
from PIL import Image
import settings # app settings (such as allowed extensions)
import functions # custom functions
@ -17,7 +19,7 @@ app = Flask(__name__) # app is the app
def allowed_extension(testext):
if testext in settings.ALLOWED_EXTENSIONS:
if testext.lower() in settings.ALLOWED_EXTENSIONS:
return True
else:
return False
@ -26,11 +28,11 @@ def allowed_extension(testext):
def log_savelog(key, ip, savedname):
if settings.SAVELOG_KEYPREFIX > 0:
with open(settings.SAVELOG, "a+") as slogf:
slogf.write("[{0}] {1}: {2} - {3}\n".format(datetime.datetime.now(), key[:settings.SAVELOG_KEYPREFIX], ip, savedname))
slogf.write(f"[{datetime.datetime.now()}] {key[:settings.SAVELOG_KEYPREFIX]}: {ip} - {savedname}\n")
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
else:
with open(settings.SAVELOG, "a+") as slogf:
slogf.write("[{0}] {1} - {2}\n".format(datetime.datetime.now(), ip, savedname))
slogf.write(f"[{datetime.datetime.now()}] {ip} - {savedname}\n")
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
@app.route("/upload", methods = ["POST"])
@ -49,6 +51,11 @@ def upload():
if request.form["uploadKey"] in validkeys: # check if uploadKey is valid
print("Key is valid!")
if "verify" in request.form.keys():
if request.form["verify"] == "true":
print("Request is asking if key is valid (it is)")
return jsonify({'status': 'key_valid'})
if "imageUpload" in request.files: # check if image to upload was provided
f = request.files["imageUpload"] # f is the image to upload
else:
@ -61,14 +68,36 @@ def upload():
fext = Path(f.filename).suffix # get the uploaded extension
if allowed_extension(fext): # if the extension is allowed
print("Generating file with extension {0}".format(fext))
fname = functions.generate_name() + fext # generate file name
print("Generated name: {0}".format(fname))
if not "imageName" in request.form.keys():
print(f"Generating file with extension {fext}")
fname = functions.generate_name() + fext # generate file name
print(f"Generated name: {fname}")
else:
fname = request.form["imageName"]
if len(fname) > 0:
print(f"Request imageName: {fname}")
if not fname.lower().endswith(fext.lower()): # if requested name doesn't have the correct extension
fname += fext # add the extension
print(f"Added extension; new filename: {fname}")
else:
print("Requested filename is blank!")
fname = functions.generate_name() + fext # generate a valid filename
print(f"Generated name: {fname}")
if f: # if the uploaded image exists
print("Uploaded image exists")
f.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image
print("Saved to {0}".format(fname))
if Path(os.path.join(settings.UPLOAD_FOLDER, fname)).is_file():
print("Requested filename already exists!")
return jsonify({'status': 'error', 'error': 'FILENAME_TAKEN'}), status.HTTP_409_CONFLICT
f.save(f"/tmp/{fname}") # save the image temporarily (before removing EXIF)
image = Image.open(f"/tmp/{fname}")
data = list(image.getdata())
stripped = Image.new(image.mode, image.size)
stripped.putdata(data)
stripped.save(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image without EXIF
print(f"Saved to {fname}")
url = settings.ROOTURL + fname # construct the url to the image
if settings.SAVELOG != "/dev/null":
print("Saving to savelog")
@ -85,17 +114,12 @@ def upload():
else: # if the key was not valid
print("Key is invalid!")
print("Request key: {0}".format(request.form["uploadKey"]))
print(f"Request key: {request.form['uploadKey']}")
return jsonify({'status': 'error', 'error': 'UNAUTHORIZED'}), status.HTTP_401_UNAUTHORIZED
else: # if uploadKey was not found in request body
print("No uploadKey found in request!")
return jsonify({'status': 'error', 'error': 'UNAUTHORIZED'}), status.HTTP_401_UNAUTHORIZED
else: # if the request method wasn't post
print("Request method was not POST!")
return jsonify({'status': 'error', 'error': 'METHOD_NOT_ALLOWED'}), status.HTTP_405_METHOD_NOT_ALLOWED
if __name__ == "__main__":
print("Run with `flask` or a WSGI server!")

View File

@ -31,7 +31,7 @@ def savekey(key):
logging.info("uploadkeys file doesn't exist, it will be created.")
with open("uploadkeys", "a+") as keyfile:
keyfile.write(str(key) + "\n") # add the key
logging.debug("Saved a key to uploadkeys: {0}".format(key))
logging.debug(f"Saved a key to uploadkeys: {key}")
def rmkey(delkey):
@ -85,14 +85,14 @@ def cmd_list(args):
if len(validkeys[i]) > 6:
showkey += "..." # add ellipses since the key was shortened in list
print(" [{0}] {1}".format(i+1, showkey))
print(f" [{i+1}] {showkey}")
def cmd_generate(args):
k = genkey(args.length)
logging.debug("Generated a new key: {0}".format(k))
logging.debug(f"Generated a new key: {k}")
savekey(k)
print("Your new key is: {0}".format(k))
print(f"Your new key is: {k}")
def cmd_add(args):
@ -104,7 +104,6 @@ def cmd_add(args):
print(ak)
if input("Is the above key correct? [y/N] ").lower() == "y":
logging.debug("Interpreted as yes")
ask_for_key = False
savekey(ak)
logging.info("Added.")
else:
@ -124,14 +123,14 @@ def cmd_dedupe(args):
for d in dupes:
r = rmkey(d)
logging.debug(r)
logging.info("Removed duplicate key: {0}".format(d))
logging.info(f"Removed duplicate key: {d}")
else:
logging.info("[" + u"\u2713" + "] No duplicate keys found!")
def cmd_show(args):
for k in get_keys():
if k[:6] == args.prefix:
print("Key: {0}".format(k))
print(f"Key: {k}")
break

View File

@ -1,2 +1,3 @@
Flask_API==2.0
Flask==1.1.2
Pillow==8.0.1

12
uwsgi.ini.default Normal file
View File

@ -0,0 +1,12 @@
[uwsgi]
socket = /srv/imgupload/uwsgi.sock
chmod-socket = 755
chdir = /srv/imgupload
venv = /srv/imgupload/env
master = true
module = imgupload:app
processes = 10
threads = 1
uid = www-data
gid = www-data
plugins = python3,logfile