├── app ├── __init__.py ├── demo.gif ├── token.pki ├── 截图上传.ahk ├── config.py ├── db_init.py ├── client.py └── server.py ├── pip-selfcheck.json ├── requirements_server.txt ├── .gitignore ├── requirements.txt └── README.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | from app import config -------------------------------------------------------------------------------- /app/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alinuxsa/MdPic/HEAD/app/demo.gif -------------------------------------------------------------------------------- /app/token.pki: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alinuxsa/MdPic/HEAD/app/token.pki -------------------------------------------------------------------------------- /app/截图上传.ahk: -------------------------------------------------------------------------------- 1 | ;快捷键 上传剪切板图片到图床 2 | !,::Run D:\workspace\MdPic\dist\client.exe -------------------------------------------------------------------------------- /pip-selfcheck.json: -------------------------------------------------------------------------------- 1 | {"last_check":"2018-09-28T04:49:00Z","pypi_version":"18.0"} -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | config = { 2 | 'username': 'demo', 3 | 'password': 'demo', 4 | 'url': 'http://127.0.0.1:18800/' 5 | } -------------------------------------------------------------------------------- /app/db_init.py: -------------------------------------------------------------------------------- 1 | from server import db, User 2 | 3 | db.create_all() 4 | u1 =User(username='demo',password='demo1') 5 | db.session.add(u1) 6 | db.session.commit() 7 | -------------------------------------------------------------------------------- /requirements_server.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | Flask==1.0.2 3 | Flask-SQLAlchemy==2.3.2 4 | itsdangerous==0.24 5 | Jinja2==2.10.1 6 | MarkupSafe==1.0 7 | SQLAlchemy==1.3.0 8 | Werkzeug==0.15.3 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | Include/* 3 | Lib/* 4 | Scripts/* 5 | tcl/* 6 | app/static/* 7 | *.db 8 | *.pyc 9 | *.spec 10 | build/* 11 | dist/* 12 | bin/* 13 | include/* 14 | lib/* 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.16.1 2 | astroid==2.0.4 3 | certifi==2018.8.24 4 | chardet==3.0.4 5 | Click==7.0 6 | clipboard==0.0.4 7 | colorama==0.3.9 8 | Flask==1.0.2 9 | Flask-SQLAlchemy==2.3.2 10 | future==0.16.0 11 | idna==2.7 12 | isort==4.3.4 13 | itsdangerous==0.24 14 | Jinja2==2.10.1 15 | lazy-object-proxy==1.3.1 16 | macholib==1.11 17 | MarkupSafe==1.0 18 | mccabe==0.6.1 19 | pefile==2018.8.8 20 | Pillow==5.2.0 21 | PyInstaller==3.4 22 | pylint==2.1.1 23 | pyperclip==1.7.0 24 | pywin32-ctypes==0.2.0 25 | requests==2.20.1 26 | six==1.11.0 27 | SQLAlchemy==1.3.0 28 | typed-ast==1.1.0 29 | Werkzeug==0.15.3 30 | urllib3==1.25.3 31 | wrapt==1.10.11 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 用FLASK实现的简单图床 2 | 3 | > 这是我的一个练手项目 4 | > 起因是七牛改了临时域名的策略,所以打算自己造一个图床 5 | > server端可以部署到自己的vps上,采用token验证 6 | > 图片名称自动使用md5加密,返回外链地址 7 | > 通过AutoHotkey调用client 8 | > 把剪切板里的图片上传到图床 返回图片地址 9 | 10 | 11 | ![](app/demo.gif) 12 | 13 | * 创建virtualenv环境 python3.6+ 14 | 15 | 略.. 16 | 17 | * 启动服务 18 | 19 | `python server.py` 20 | 21 | * 修改文件路径 22 | 23 | client.exe需要使用pyinstaller打包 24 | 25 | `pyinstaller -F client.py` 26 | 27 | 根据实际情况修改 `截图上传.ahk` 里面 client.exe的路径, 以及自定义快捷键 28 | 29 | * 上传图片 30 | 31 | 32 | 通过QQ截图 33 | 34 | 通过快捷键 `Alt + ,` 35 | 36 | 调用client上传, 成功后图片地址写在剪切板里,直接粘贴即可 37 | 38 | * AutoHotkey 官网 39 | 40 | [传送门](https://autohotkey.com/) 41 | 42 | * 延伸 43 | 44 | 使用dropbox备份图片 45 | 46 | nginx反向代理,图片压缩,开启https 47 | 48 | cloudflare cdn缓存 49 | -------------------------------------------------------------------------------- /app/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from PIL import ImageGrab, Image 4 | import clipboard, pickle 5 | import time, os 6 | from app.config import config 7 | 8 | class Client(): 9 | """获取token""" 10 | def __init__(self): 11 | self.url = config.get('url', None) 12 | self.username = config.get('username', None) 13 | self.password = config.get('password', None) 14 | self.headers = {'Content-Type': 'application/json'} 15 | self.payload = {'username':self.username, 'password':self.password} 16 | self.pkiFile = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'token.pki') 17 | 18 | def getToken(self): 19 | # 获取token 20 | token = {} 21 | r = requests.post(self.url,headers=self.headers, data=json.dumps(self.payload)) 22 | t = r.text 23 | timestamp = int(time.time()) 24 | token.setdefault('token', t) 25 | token.setdefault('timestamp', timestamp) 26 | with open(self.pkiFile, 'wb') as f: 27 | pickle.dump(token,f) 28 | 29 | def showToken(self): 30 | # 从文件获取token 31 | with open(self.pkiFile, 'rb') as f: 32 | t = pickle.load(f) 33 | return t.get('token') 34 | 35 | def tokenExpired(self): 36 | # 验证是否过期 37 | if not os.path.exists(self.pkiFile): 38 | self.getToken() 39 | with open(self.pkiFile, 'rb') as f: 40 | token = pickle.load(f) 41 | if int(time.time()) - token.get('timestamp') > 600: 42 | return True 43 | else: 44 | return False 45 | 46 | 47 | def upload(self): 48 | # 从剪切板获取图片上传 49 | 50 | upload_url = self.url + 'upload' 51 | files = {'image001': open(r'd:/tmp0001.jpg', 'rb')} 52 | h1 = {'token':token} 53 | ru = requests.post(upload_url, headers=h1, files=files) 54 | clipboard.copy(ru.text) 55 | 56 | if __name__ == '__main__': 57 | im= ImageGrab.grabclipboard() 58 | if isinstance(im, Image.Image): 59 | print("剪切板有图片") 60 | c = Client() 61 | if c.tokenExpired(): 62 | print('token过期,重新获取') 63 | c.getToken() 64 | token = c.showToken() 65 | im.save(r'd:/tmp0001.jpg') 66 | c.upload() -------------------------------------------------------------------------------- /app/server.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from flask import Flask, request, jsonify, url_for 3 | from flask_sqlalchemy import SQLAlchemy 4 | from werkzeug.security import check_password_hash, generate_password_hash 5 | from werkzeug.utils import secure_filename 6 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, SignatureExpired, BadSignature 7 | import os, time 8 | import hashlib 9 | 10 | 11 | app = Flask(__name__) 12 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///api.db' 13 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True 14 | app.config['SECRET_KEY'] = 'guess me' 15 | app.config['JSON_AS_ASCII'] = False 16 | app.config['UPLOAD_FOLDER'] = 'static/' 17 | app.config['ALLOWED_EXTENSIONS'] = set(['png', 'jpg', 'jpeg', 'gif']) 18 | db = SQLAlchemy(app) 19 | 20 | msg_default = { 21 | "msg": "invalid args" 22 | } 23 | 24 | def allow_file(filename): 25 | return '.' in filename and \ 26 | filename.rsplit('.', 1)[1] in app.config['ALLOWED_EXTENSIONS'] 27 | 28 | 29 | def gen_filename(filename): 30 | file_format = filename.split('.')[-1] 31 | timestamp = time.time() 32 | tmp_str = '{}{}'.format(timestamp, filename) 33 | return (hashlib.md5(tmp_str.encode('utf-8')).hexdigest()) + "." + file_format 34 | 35 | 36 | 37 | class User(db.Model): 38 | __tablename__ = 'user' 39 | id = db.Column(db.Integer, primary_key=True) 40 | username = db.Column(db.String(120), nullable=False, unique=True) 41 | password_hash = db.Column(db.String(200)) 42 | 43 | def gen_auth_token(self, expiration=600): 44 | s = Serializer(app.config['SECRET_KEY'], expires_in=expiration) 45 | return s.dumps({'id': self.id}) 46 | 47 | @staticmethod 48 | def verify_auth_token(token): 49 | s = Serializer(app.config['SECRET_KEY']) 50 | try: 51 | data = s.loads(token) 52 | except SignatureExpired: 53 | # token过期 54 | return None 55 | except BadSignature: 56 | # token无效 57 | return None 58 | user = User.query.get(data['id']) 59 | return user 60 | 61 | @property 62 | def password(self): 63 | raise AttributeError('password is not a readable attribute') 64 | 65 | def verify_password(self, password): 66 | # 验证密码 67 | return check_password_hash(self.password_hash, password) 68 | 69 | @password.setter 70 | def password(self, password): 71 | # 生成密码 72 | self.password_hash = generate_password_hash(password) 73 | 74 | # 获取token 75 | @app.route('/', methods=['POST', 'GET']) 76 | def index(): 77 | if request.method == 'POST': 78 | data = request.get_json() 79 | username = data.get('username', None) 80 | password = data.get('password', None) 81 | print(username, password) 82 | if username and password: 83 | user = User.query.filter_by(username=username).first() 84 | if user is not None and user.verify_password(password): 85 | token = user.gen_auth_token() 86 | return token 87 | else: 88 | return jsonify(msg_default) 89 | else: 90 | return jsonify(msg_default) 91 | else: 92 | return jsonify(msg_default) 93 | 94 | # 上传图片 95 | @app.route('/upload', methods=['POST']) 96 | def upload(): 97 | if request.method == 'POST': 98 | upload_file = request.files['image001'] 99 | token = request.headers.get('token') 100 | u1 = User.verify_auth_token(token) 101 | if u1 is not None and u1 is not False: 102 | print("token有效") 103 | if upload_file: 104 | filename = gen_filename(upload_file.filename) 105 | print("获取到filename {}".format(filename)) 106 | upload_file.save(os.path.join(app.root_path, app.config['UPLOAD_FOLDER'], filename)) 107 | ext_link = url_for('static', filename=filename, _external=True, _scheme='https') 108 | return ext_link 109 | else: 110 | msg = {"msg":"invalid token"} 111 | return jsonify(msg) 112 | else: 113 | return jsonify(msg_default) 114 | 115 | 116 | if __name__ == '__main__': 117 | app.run(host='127.0.0.1', port=18800, debug=False) 118 | 119 | --------------------------------------------------------------------------------