本文导入:近期在使用NAS的时候,想将NAS上的文件共享给好友,但好友没有连接NAS的客户端,我也不想留下NAS的账户名和密码,于是想到使用直链来分享,网上并没有好的解决方案,遂写了一个并开源
代码解析
#---导入目标库---the libraries you need to import
import datetime
from flask import Flask, redirect,render_template,request, url_for
from flask_login import LoginManager, current_user,login_user,UserMixin,login_required, logout_user
import sqlite3
import hashlib
import yaml
import os
#---基础配置---base settings---
#特别说明,本程序并不针对多用户服务开发,仅支持单用户管理
#Explain:this program is not developed for multipe users.Only support one user.
class User(UserMixin):
def __init__(self,user_id):
self.id = user_id
app = Flask(__name__)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
app.secret_key = str(os.environ.get('FLASK_SECRET_KEY')) #密钥(secret key)
app.config['PORT'] = int(config['port']) #服务端口(server port)
app.config['DATABASE'] = str(config['database']) #数据库名和位置(database name and location)
app.config['ROOT'] = str(config['root']) #分享文件根目录(the files root path which you need to share.Usually the NAS root path)
app.config['READ'] = int(config['read']) #SHA计算时每次读取的文件大小,默认8KB(the sha256 calculator read file size each time.Default 8KB)
app.config['PASSWORD'] = str(config['password']) #用户密码(User's password)
#---基本函数---base functions---
@login_manager.user_loader
def load_user(user_id):
return User(1)
def opreat_db():#数据库操作基本函数(database opreation function)
con = sqlite3.connect(app.config['DATABASE'])
con.row_factory = sqlite3.Row
return con
def sha_calculate(file_path):#sha256计算器(sha256 calculator)
if not os.path.isfile(file_path):
print("文件不存在。")
return None
sha512_256 = hashlib.sha256()
with open(file_path, 'rb') as f:
while chunk := f.read(app.config['READ']*8):
sha512_256.update(chunk)
return sha512_256.hexdigest()
#---路由设置---route settings---
@app.route('/login', methods=['GET', 'POST'])#登录(login)
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
password = request.form['pwd']
if password == app.config['PASSWORD']:
user = User(1)
login_user(user)
return redirect(url_for('dashboard'))
else:
msg = "密码错误!"#Error message!Password is not correct
return render_template('result.html',msg=msg)
return render_template('login.html')
@app.route('/logout')#登出(logout)
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route("/dashboard")#仪表盘逻辑(dashboard logic)
@login_required
def dashboard():
con = opreat_db()
cur = con.cursor()
cur.execute("SELECT * FROM shares")
rows = cur.fetchall()
total = con.execute('SELECT COUNT(*) as count FROM shares').fetchone()['count']
con.close()
return render_template("dashboard.html",rows = rows,total = total)
@login_manager.unauthorized_handler
def unauthorized():
return '您没有权限访问!'
@app.route("/create")#单击创建分享时的新建分享页面(page which you click ths create button)
@login_required
def create():
return render_template('create.html')
@login_manager.unauthorized_handler
def unauthorized():
return '您没有权限访问!'
@app.route("/<sha256>/delet",methods=['GET'])
@login_required
def Delet(sha256):
db = opreat_db()
share = db.execute('SELECT * FROM shares WHERE sha256 = ?', (sha256,)).fetchone()
if not share:
db.close()
msg = '共享不存在或已被删除'
return render_template('result.html',msg=msg)
db.execute('DELETE FROM shares WHERE sha256 = ?', (sha256,))
db.commit()
db.close()
msg = '共享删除成功'
return render_template('result.html',msg=msg)
@login_manager.unauthorized_handler
def unauthorized():
return '您没有权限访问!'
@app.route('/result',methods = ['GET', 'POST'])#接受创建表单并返回结果(receive ths request and return results)
@login_required
def Add():
msg = '已成功创建分享'#Success message!
path = request.form['path']
if not os.path.exists(path):
msg = '指定的文件路径不存在!'#Error message!the path is not existed
return render_template('result.html',msg=msg)
sha256 = sha_calculate(path)
name = os.path.basename(path)
expire_days = int(request.form['days'])
expire_time = (datetime.datetime.now() + datetime.timedelta(days=expire_days)).strftime('%Y-%m-%d %H:%M:%S')
Max = request.form['max']
try:
db = opreat_db()
db.execute('''
INSERT INTO shares
(sha256, file_path, file_name, expire_time, max_downloads)
VALUES (?, ?, ?, ?, ?)
''', (sha256, path, name, expire_time, Max))
db.commit()
db.close()
return render_template('result.html',msg = msg)
except sqlite3.IntegrityError:
msg = '已共享该文件!'#Error Message!the file is repeated
return render_template('result.html',msg=msg)
except Exception as e:#Error Message!database error
msg = '数据库错误!抛出异常:<br>'+str(e)
return render_template('result.html',msg=msg)
@login_manager.unauthorized_handler
def unauthorized():
return '您没有权限访问!'
if __name__ == "__main__":
app.run(port=app.config['PORT'],debug=False)
对应注释写的很清楚,因为要开源到github上因此注释作了双语版的
本部分代码是control.py的,用于网页管理分享,需要访问“地址/login”才能进入登录界面,特意设置的直接访问是进不去的
#---导入目标库---the libraries you need to import
from flask import Flask, send_from_directory, abort
import sqlite3
import yaml
from datetime import datetime
import os
#---基础配置---base settings---
app = Flask(__name__)
with open('config.yaml', 'r') as f:
config = yaml.safe_load(f)
app.config['DATABASE'] = str(config['database'])
app.config['ROOT'] = str(config['root'])
#---基本函数---base functions---
def get_file_path(sha256):
"""查询数据库,返回文件路径及限制信息"""
conn = sqlite3.connect(app.config['DATABASE'])
cursor = conn.cursor()
cursor.execute("""
SELECT file_path, expire_time, max_downloads, current_downloads
FROM shares WHERE sha256 = ?
""", (sha256,))
result = cursor.fetchone()
conn.close()
if not result:
return None # 哈希不存在
file_path, expire_time, max_downloads, current_downloads = result
# 检查过期时间
if expire_time and datetime.now() > datetime.fromisoformat(expire_time):
return "expired"
# 检查下载次数
if max_downloads and current_downloads >= max_downloads:
return "max_downloads"
# 更新下载次数
conn = sqlite3.connect(app.config['DATABASE'])
cursor = conn.cursor()
cursor.execute("""
UPDATE shares SET current_downloads = current_downloads + 1
WHERE sha256 = ?
""", (sha256,))
conn.commit()
conn.close()
return file_path
#---路由设置---route settings---
@app.route('/<sha256>')
def download_file(sha256):
file_path = get_file_path(sha256)
if file_path is None:
abort(404, description="文件不存在或已取消共享")
if file_path in ["expired", "max_downloads"]:
abort(403, description="文件已过期或超过最大下载次数")
if not file_path.startswith(app.config['ROOT']):
abort(403, description="非法文件路径")
dir_path = os.path.dirname(file_path)
file_name = os.path.basename(file_path)
return send_from_directory(dir_path, file_name, as_attachment=True)
if __name__ == '__main__':
app.run(port=5000, debug=False)
本部分是download.py,用于处理下载请求,在config.yaml输入的NAS根目录就是为了防止遍历目录攻击,同样的注释已经写的十分清楚
使用说明
下面将演示在linux中运行本服务
首先在github的Releases中找到最新版的.tar.gz包,我们下载它,或者使用wget命令下载
wget https://github.com/super-inventor/NASDirectLinkShareTool/releases/download/official/Linux-0.1.0.tar.gz
注意:因为一些广为人知的原因,下载速率过慢很正常,本项目也不大,略微等待即可
下面我们来解压
tar -zxvf ./Linux-0.1.0.tar.gz
这个文件夹名不好听,并且里面嵌套了一层文件夹(我保证我会在下一个版本修复这一问题)于是乎我们把文件全部移出来,并给文件夹起一个好听的名字
cd ./Linux-0.1.0/NAS直链分享工具
mv ./* ../
mv ./templates ../
cd ../
rm -r ./NAS直链分享工具
cd ../
mv ./Linux-0.1.0 ./NDLT
完成之后,我们来检查一些系统是否安装了Python虚拟环境,笔者是 Debian系用户,因此这里以Ubuntu为例
sudo apt install python3-venv
如果没安装会自动安装,安装完之后我们就可以进入文件夹,运行install.sh来自动化配置了
cd ./NDLT
bash ./install.sh

笔者这里因为已经配置过一遍,所以什么就几句话,实际第一次配置会引入很多的库,所以会比这个长
注意,我们还没有完成配置工作,我们需要在templates里面找到dashboard.html里面找到第109行,修改里面的<>内容,你也发现了本项目是两个py文件,端口是分开的,目的就是为了防止路径遍历攻击而盗取数据
<!--注意!请在此处填写您的下载地址!--><a href="<你的下载地址>/{{ row["sha256"] }}"
这个下载地址是指向5000端口的域名或者IPv4地址:5000也可以(我不推荐这么做,宁肯多部署一个反向代理)
在这之后,我们便可以开启服务了
bash ./start.sh

出现形如这样的输出,并且有两遍,就是启动成功了,停止使用
bash ./stop.sh
即可停止服务
使用效果



本项目可以高度自定义,修改html文件即可更改样式,整体用下来十分方便,以后点对点分享文件不需要用百度网盘等等,只需要传到NAS上,点一下创建共享就可以拿到直链,享受全速下载
不足之处
本项目仍然有不足之处,不支持密码下载等等功能的缺失,如果您有什么好的意见欢迎通过Issue提交,项目采用Apache2.0协议,您也可以直接提交修改,欢迎与我交流!