Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
9910bc279c | |||
f100732e4d | |||
46420eecda | |||
7b372a7b6f | |||
5570710432 | |||
ba68674e4e
|
|||
99ff2c68a3 | |||
dd069bf395 | |||
f21adfa04e | |||
3d5c55498f | |||
5e2be10434 | |||
7c1f449bce | |||
0dbcc0e380 | |||
b8b5a2518c | |||
805e545b39 | |||
9a117817f7 | |||
c9cd6469a9
|
|||
6f64890e34
|
|||
91db522363
|
|||
9d9b93a9ee
|
|||
565a91e4ec
|
|||
3fcdaa2b10
|
|||
4309225185
|
|||
065296f84a
|
|||
841bb513d3
|
|||
f0bb30a747 | |||
7fce3f57e9 | |||
a587040809 | |||
8a95dbb0fa | |||
a5a22b7c88
|
|||
7ccaafc6c6
|
|||
797bebb1a1
|
|||
08f9e13da0 | |||
3d1304b3b0 | |||
4b624f3fed
|
7
.github/README.md
vendored
Normal file
7
.github/README.md
vendored
Normal 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.
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -132,3 +132,5 @@ dmypy.json
|
||||
uploadkeys
|
||||
savelog.log
|
||||
uwsgi.log
|
||||
settings.py
|
||||
functions.py
|
||||
|
87
README.md
87
README.md
@ -1,4 +1,87 @@
|
||||
# imgupload
|
||||
Python Flask uWSGI application to receive and save images over POST requests.
|
||||
|
||||
This project is still in development. Use at your own risk!
|
||||
<!---
|
||||

|
||||

|
||||

|
||||

|
||||
-->
|
||||
|
||||
## What is imgupload?
|
||||
|
||||
imgupload is a Flask + uWSGI application to serve as an all-purpose image/file uploader over POST requests.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
### Using uWSGI
|
||||
|
||||
Note: replace `www-data` with whatever user your webserver runs as.
|
||||
|
||||
1. Make /srv/imgupload: `sudo mkdir /srv/imgupload`
|
||||
2. Change ownership of /srv/imgupload: `sudo chown www-data:www-data /srv/imgupload`
|
||||
3. Enter www-data user: `sudo su www-data`
|
||||
4. Change directories to /srv/imgupload: `cd /srv/imgupload`
|
||||
5. Clone the repository: `git clone https://git.bbaovanc.com/bbaovanc/imgupload.git`
|
||||
6. Enter the imgupload directory: `cd imgupload`
|
||||
7. Create a virtualenv: `python3 -m venv env`
|
||||
8. Enter the virtualenv: `source env/bin/activate`
|
||||
9. Install dependencies: `python3 -m pip install -r requirements.txt`
|
||||
10. Leave the www-data user: `exit`
|
||||
11. Copy the default uWSGI configuration: `sudo cp /srv/imgupload/uwsgi.ini.default /etc/uwsgi/apps-available/imgupload.ini`
|
||||
12. Modify `/etc/uwsgi/apps-available/imgupload.ini` to your preferences
|
||||
13. Enable imgupload: `sudo ln -s /etc/uwsgi/apps-available/imgupload.ini /etc/uwsgi/apps-enabled/`
|
||||
14. Restart uWSGI: `sudo systemctl restart uwsgi`
|
||||
15. 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
|
||||
$ 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)
|
||||
|
@ -1,3 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
configtest.py
|
||||
|
||||
Tests the validity of your configuration in settings.py.
|
||||
"""
|
||||
|
||||
import os
|
||||
import settings as settings
|
||||
|
||||
@ -9,7 +16,6 @@ defaults = {
|
||||
"ROOTURL": "https://img.bbaovanc.com/",
|
||||
"SAVELOG": "savelog.log",
|
||||
"SAVELOG_CHMOD": "0o644",
|
||||
"UPLOADKEYS_CHMOD": "0o400",
|
||||
"SAVELOG_KEYPREFIX": 4,
|
||||
}
|
||||
|
||||
@ -19,7 +25,6 @@ deftypes = {
|
||||
"ROOTURL": str,
|
||||
"SAVELOG": str,
|
||||
"SAVELOG_CHMOD": int,
|
||||
"UPLOADKEYS_CHMOD": int,
|
||||
"SAVELOG_KEYPREFIX": int,
|
||||
}
|
||||
|
||||
@ -80,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
|
||||
|
15
functions.py.default
Normal file
15
functions.py.default
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
functions.py
|
||||
|
||||
Functions used by imgupload which can be easily customized.
|
||||
"""
|
||||
|
||||
import string
|
||||
import random
|
||||
|
||||
|
||||
def generate_name():
|
||||
chars = string.ascii_letters + string.digits # uppercase, lowercase, and numbers
|
||||
name = ''.join((random.choice(chars) for i in range(8))) # generate name
|
||||
return name
|
75
imgupload.py
75
imgupload.py
@ -1,35 +1,29 @@
|
||||
from flask import Flask, request, jsonify, abort, Response
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
imgupload.py
|
||||
|
||||
Flask application for processing images uploaded through POST requests.
|
||||
"""
|
||||
|
||||
from flask import Flask, request, jsonify, Response
|
||||
from flask_api import status
|
||||
from pathlib import Path
|
||||
import string
|
||||
import random
|
||||
import os
|
||||
import datetime
|
||||
|
||||
import settings # app settings (such as allowed extensions)
|
||||
|
||||
|
||||
ALPHANUMERIC = string.ascii_letters + string.digits # uppercase, lowercase, and numbers
|
||||
import functions # custom functions
|
||||
|
||||
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
|
||||
|
||||
|
||||
def generate_name(extension):
|
||||
namefound = False
|
||||
while not namefound:
|
||||
fname = ''.join((random.choice(ALPHANUMERIC) for i in range(8))) + str(extension)
|
||||
if not Path(fname).is_file():
|
||||
namefound = True
|
||||
return fname
|
||||
|
||||
|
||||
def log_savelog(key, ip, savedname):
|
||||
if settings.SAVELOG_KEYPREFIX > 0:
|
||||
with open(settings.SAVELOG, "a+") as slogf:
|
||||
@ -40,30 +34,29 @@ def log_savelog(key, ip, savedname):
|
||||
slogf.write("[{0}] {1} - {2}\n".format(datetime.datetime.now(), ip, savedname))
|
||||
os.chmod(settings.SAVELOG, settings.SAVELOG_CHMOD)
|
||||
|
||||
|
||||
@app.route("/upload", methods = ["POST"])
|
||||
def upload():
|
||||
if request.method == "POST": # sanity check: make sure it's a POST request
|
||||
print("Request method was POST!")
|
||||
|
||||
os.chmod("uploadkeys", settings.UPLOADKEYS_CHMOD)
|
||||
print("Changed permissions of `uploadkeys`")
|
||||
with open("uploadkeys", "r") as keyfile: # load valid keys
|
||||
validkeys = keyfile.readlines()
|
||||
validkeys = [x.strip("\n") for x in validkeys]
|
||||
while "" in validkeys:
|
||||
validkeys.remove("")
|
||||
print("removed blank key")
|
||||
print("Valid keys: {0}".format(validkeys))
|
||||
print("Loaded validkeys")
|
||||
|
||||
if "uploadKey" in request.form: # if an uploadKey was provided
|
||||
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
|
||||
print("Found uploaded image")
|
||||
else:
|
||||
print("No image upload was found!")
|
||||
return jsonify({'status': 'error', 'error': 'NO_IMAGE_UPLOADED'}), status.HTTP_400_BAD_REQUEST
|
||||
@ -73,42 +66,52 @@ def upload():
|
||||
return jsonify({'status': 'error', 'error': 'FILENAME_BLANK'}), status.HTTP_400_BAD_REQUEST
|
||||
|
||||
fext = Path(f.filename).suffix # get the uploaded extension
|
||||
print("Uploaded file extensions is {0}".format(fext))
|
||||
if allowed_extension(fext): # if the extension is allowed
|
||||
fname = generate_name(fext) # generate file name
|
||||
print("Generated name: {0}".format(fname))
|
||||
if not "imageName" in request.form.keys():
|
||||
print("Generating file with extension {0}".format(fext))
|
||||
fname = functions.generate_name() + fext # generate file name
|
||||
print("Generated name: {0}".format(fname))
|
||||
else:
|
||||
fname = request.form["imageName"]
|
||||
if len(fname) > 0:
|
||||
print("Request imageName: {0}".format(fname))
|
||||
if not fname.lower().endswith(fext.lower()): # if requested name doesn't have the correct extension
|
||||
fname += fext # add the extension
|
||||
print("Added extension; new filename: {0}".format(fname))
|
||||
else:
|
||||
print("Requested filename is blank!")
|
||||
fname = functions.generate_name() + fext # generate a valid filename
|
||||
print("Generated name: {0}".format(fname))
|
||||
|
||||
if f: # if the uploaded image exists
|
||||
print("Uploaded image exists, obviously.")
|
||||
print("Uploaded image exists")
|
||||
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(os.path.join(settings.UPLOAD_FOLDER, fname)) # save the image
|
||||
print("Saved to {0}".format(fname))
|
||||
url = settings.ROOTURL + fname # construct the url to the image
|
||||
if settings.SAVELOG != "/dev/null":
|
||||
print("Saving to savelog")
|
||||
log_savelog(request.form["uploadKey"], request.remote_addr, fname)
|
||||
print("Logged message to savelog")
|
||||
print("Returning json response")
|
||||
return jsonify({'status': 'success', 'url': url, 'name': fname, 'uploadedName': f.filename}), status.HTTP_201_CREATED
|
||||
else: # this shouldn't happen
|
||||
print("Um... uploaded image is nonexistent..? Please report this error!")
|
||||
print("Um... uploaded image... is nonexistent? Please report this error!")
|
||||
return jsonify({'status': 'error', 'error': 'UPLOADED_IMAGE_FAILED_SANITY_CHECK_1'}), status.HTTP_400_BAD_REQUEST
|
||||
|
||||
else: # if the extension was invalid
|
||||
print("Uploaded extension is invalid!")
|
||||
abort(415)
|
||||
return jsonify({'status': 'error', 'error': 'INVALID_EXTENSION'}), status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
|
||||
else: # if the key was not valid
|
||||
print("Key is invalid!")
|
||||
print("Request key: {0}".format(request.form["uploadKey"]))
|
||||
abort(401)
|
||||
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!")
|
||||
abort(401)
|
||||
|
||||
|
||||
else: # if the request method wasn't post
|
||||
print("Request method was not POST!")
|
||||
abort(405)
|
||||
return jsonify({'status': 'error', 'error': 'UNAUTHORIZED'}), status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Run with `flask` or a WSGI server!")
|
||||
|
181
keyctl.py
Normal file
181
keyctl.py
Normal file
@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
keyctl.py
|
||||
|
||||
Command-line utility for easy management of the uploadkeys file.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
|
||||
|
||||
def read_keyfile():
|
||||
with open("uploadkeys", "r") as keyfile: # open uploadkeys
|
||||
keys = keyfile.readlines() # read all the keys
|
||||
logging.debug("Read uploadkeys")
|
||||
keys = [x.strip("\n") for x in keys] # strip newlines from keys
|
||||
logging.debug("Stripped newlines from keys")
|
||||
return keys
|
||||
|
||||
|
||||
def genkey(length):
|
||||
key = ''.join(secrets.choice(string.ascii_letters + string.digits) for x in range(length))
|
||||
return key
|
||||
|
||||
|
||||
def savekey(key):
|
||||
if not Path("uploadkeys").is_file(): # if uploadkeys doesn't exist, log an info message
|
||||
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))
|
||||
|
||||
|
||||
def rmkey(delkey):
|
||||
removedkey = False
|
||||
allkeys = read_keyfile()
|
||||
if delkey in allkeys: # if the key to remove exists
|
||||
allkeys.remove(delkey) # remove the first instance of the key
|
||||
removedkey = True
|
||||
logging.debug("Removed one instance of the key")
|
||||
|
||||
with open("uploadkeys", "w") as keyfile:
|
||||
for k in allkeys:
|
||||
keyfile.write(k + "\n") # write the remaining keys
|
||||
|
||||
if removedkey:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def find_duplicates():
|
||||
allkeys = read_keyfile()
|
||||
|
||||
seen = set()
|
||||
ukeys = []
|
||||
dupkeys = []
|
||||
for x in allkeys:
|
||||
if x not in seen:
|
||||
ukeys.append(x)
|
||||
seen.add(x)
|
||||
else:
|
||||
dupkeys.append(x)
|
||||
return dupkeys
|
||||
|
||||
|
||||
def get_keys():
|
||||
validkeys = read_keyfile()
|
||||
while "" in validkeys:
|
||||
validkeys.remove("")
|
||||
logging.debug("Removed blank keys")
|
||||
return validkeys
|
||||
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
validkeys = get_keys()
|
||||
|
||||
print("List of upload keys:")
|
||||
for i in range(len(validkeys)):
|
||||
showkey = validkeys[i][:6]
|
||||
if len(validkeys[i]) > 6:
|
||||
showkey += "..." # add ellipses since the key was shortened in list
|
||||
|
||||
print(" [{0}] {1}".format(i+1, showkey))
|
||||
|
||||
|
||||
def cmd_generate(args):
|
||||
k = genkey(args.length)
|
||||
logging.debug("Generated a new key: {0}".format(k))
|
||||
savekey(k)
|
||||
print("Your new key is: {0}".format(k))
|
||||
|
||||
|
||||
def cmd_add(args):
|
||||
print("Please type/paste the key you would like to add.")
|
||||
akr = input("> ")
|
||||
ak = akr.strip()
|
||||
print()
|
||||
logging.debug("Ran strip() on key")
|
||||
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:
|
||||
logging.debug("Interpreted as no")
|
||||
print("No key has been saved.")
|
||||
|
||||
|
||||
def cmd_remove(args):
|
||||
if rmkey(args.key):
|
||||
logging.debug("Successfully removed the requested key")
|
||||
else:
|
||||
logging.info("No key was removed.")
|
||||
|
||||
def cmd_dedupe(args):
|
||||
dupes = find_duplicates()
|
||||
if len(dupes) > 0:
|
||||
for d in dupes:
|
||||
r = rmkey(d)
|
||||
logging.debug(r)
|
||||
logging.info("Removed duplicate key: {0}".format(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))
|
||||
break
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser() # create instance of argument parser class
|
||||
|
||||
parlog = parser.add_mutually_exclusive_group()
|
||||
parlog.add_argument("-v", "--verbose", help="show debugging messages", action="store_true")
|
||||
parlog.add_argument("-q", "--quiet", help="show only warning messages and up", action="store_true")
|
||||
|
||||
subparsers = parser.add_subparsers(help="sub-commands")
|
||||
parser_list = subparsers.add_parser("list", help="list the beginning of each key")
|
||||
parser_list.set_defaults(func=cmd_list)
|
||||
|
||||
parser_gen = subparsers.add_parser("generate", help="generate a key and save it to uploadkeys")
|
||||
parser_gen.add_argument("length", help="length of key to generate", default=64, type=int, nargs="?")
|
||||
parser_gen.set_defaults(func=cmd_generate)
|
||||
|
||||
parser_add = subparsers.add_parser("add", help="prompts for a key to add to uploadkeys")
|
||||
parser_add.set_defaults(func=cmd_add)
|
||||
|
||||
parser_remove = subparsers.add_parser("remove", help="remove (one instance of) a key from uploadkeys")
|
||||
parser_remove.add_argument("key", help="key to remove")
|
||||
parser_remove.set_defaults(func=cmd_remove)
|
||||
|
||||
parser_dedupe = subparsers.add_parser("dedupe", help="remove duplicate keys")
|
||||
parser_dedupe.set_defaults(func=cmd_dedupe)
|
||||
|
||||
parser_show = subparsers.add_parser("show", help="show the full key based on the first 6 characters")
|
||||
parser_show.add_argument("prefix", help="first 6 characters of key (shown by `python3 keyctl.py list`)")
|
||||
parser_show.set_defaults(func=cmd_show)
|
||||
|
||||
|
||||
args = parser.parse_args() # parse the arguments
|
||||
|
||||
if args.verbose:
|
||||
loglevel = logging.DEBUG
|
||||
elif args.quiet:
|
||||
loglevel = logging.WARNING
|
||||
else:
|
||||
loglevel = logging.INFO
|
||||
logging.basicConfig(level=loglevel, format="%(levelname)s: %(message)s")
|
||||
|
||||
try:
|
||||
args.func(args)
|
||||
except AttributeError:
|
||||
logging.error("AttributeError")
|
||||
parser.print_help()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
Flask_API==2.0
|
||||
Flask==1.1.2
|
@ -1,7 +1,13 @@
|
||||
UPLOAD_FOLDER = "/var/www/img"
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
settings.py
|
||||
|
||||
User-defined settings used by imgupload.py.
|
||||
"""
|
||||
|
||||
UPLOAD_FOLDER = "/path/to/images"
|
||||
ALLOWED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".svg", ".bmp", ".gif", ".ico", ".webp"]
|
||||
ROOTURL = "https://img.bbaovanc.com/"
|
||||
ROOTURL = "https://example.com/"
|
||||
SAVELOG = "savelog.log"
|
||||
SAVELOG_CHMOD = 0o644
|
||||
UPLOADKEYS_CHMOD = 0o600
|
||||
SAVELOG_KEYPREFIX = 4
|
12
uwsgi.ini.default
Normal file
12
uwsgi.ini.default
Normal 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
|
Reference in New Issue
Block a user