前言
还记得那个在局域网内即开即用、为你省去文件传输烦恼的FTP小工具吗?在收获了许多朋友的积极反馈后,我们迎来了又一次重要更新!V2.3版本聚焦于大家最关注的三个核心诉求:安全、效率和条理,致力于让你的文件共享体验既强大又省心。

(note: 其他局域网内的设备,请访问 192.168.xx.xx:5000那个地址即可。)
本次升级主要包含三大核心功能:
🔐 密钥验证,为隐私加把锁:彻底告别"网络邻居皆可访问"的裸奔时代!我们为网站增加了简单的API密钥验证机制。现在,只有持有密钥的设备才能接入,有效防止在公共网络环境下的未授权访问,让你的私人文件更加安全。
🚀 批量上传,效率飞跃:一次选中上百张照片或大量文件,即可自动完成上传!我们优化了上传逻辑,无论是备份手机相册还是同步工作资料,都能大幅节省你的时间和操作步骤。
📂 下载页分类,井然有序:面对海量文件不再迷茫。下载页面新增了文件分类功能,你可以清晰地从不同类别中快速定位所需,让文件管理变得条理清晰。
此次更新,标志着我们这个轻量级工具从"可用"向"好用、安心"迈出了坚实的一步。知足而上进,温柔且坚定,我们将继续用代码优化身边的世界。
友好提示:大家也可以把想要新增的功能写在评论区上,我有空会查看评论区,有空闲时间也会适当更新一版,让我们的工具更好用。
希望V2.3版本能给你带来更出色的体验!
往期版本
一步一个脚印,up本身工作也比较忙,只好空闲抽点娱乐时间来谢谢咦~友友们见谅.
- FTP局域网功能小网站V2_2, 增加密钥鉴权
- FTP局域网功能小网站V2_1, UI大升级
- ftp功能的小网站(flask实现) V1, 不推荐,界面不好看
效果展示
手机端
进入时需要输入密钥,密钥在app.py中确定

最大传输文件大小限制在upload.html中修改

上传成功后会有图标提示

下载页面, 暂时是按照类型分类,若友友们有什么好提议,可以在评论区和我说。

展示的文件名过长的话,你们可以使用横屏,把手机横过来看

电脑端
大部分差不多,我就不一一列了。可以参考下之前版本的
最大传输文件大小限制在upload.html中修改

文件名过长我这里加了个小滑杆。

代码
download_page.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>下载文件 - FTP功能小网站</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 配置Tailwind自定义颜色 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5',
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.file-item-hover {
@apply bg-blue-50;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-teal-50 min-h-screen font-sans text-gray-800">
<div class="container mx-auto px-4 py-8 max-w-4xl relative">
<!-- 登出按钮 -->
<div class="absolute top-0 right-0 mt-2 mr-2">
<a href="{{ url_for('logout') }}" class="inline-flex items-center text-gray-500 hover:text-primary transition-colors px-3 py-2 rounded-lg hover:bg-primary/5">
<i class="fa fa-sign-out mr-1"></i>
<span>登出</span>
</a>
</div>
<!-- 页面标题 -->
<header class="text-center mb-10 mt-6">
<h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-primary mb-2">
<i class="fa fa-cloud-download mr-3"></i>文件下载
</h1>
<p class="text-gray-500">选择您需要下载的文件</p>
</header>
<!-- 面包屑导航 -->
<div class="mb-4">
<nav class="flex" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3">
<!-- 根目录 -->
<li>
<a href="{{ url_for('download_page') }}" class="inline-flex items-center text-sm font-medium text-gray-700 hover:text-primary">
<i class="fa fa-folder mr-2"></i>
<span>文件</span>
</a>
</li>
<!-- 子目录 -->
{% if breadcrumbs %}
{% for crumb in breadcrumbs %}
<li>
<div class="flex items-center">
<i class="fa fa-chevron-right text-gray-400 mx-2 text-xs"></i>
<a href="{{ url_for('download_page', dir=crumb.path) }}" class="text-sm font-medium text-gray-700 hover:text-primary">
{{ crumb.name }}
</a>
</div>
</li>
{% endfor %}
{% endif %}
</ol>
</nav>
</div>
<!-- 文件列表卡片 -->
<div class="bg-white rounded-xl shadow-md overflow-hidden mb-8">
<!-- 列表头部 -->
<div class="bg-gray-50 px-6 py-4 border-b border-gray-100">
<h2 class="font-semibold text-gray-700">
{% if current_dir %}
{{ current_dir.split('/')[-1] }} 中的内容
{% else %}
可供下载的文件
{% endif %}
</h2>
</div>
<!-- 文件列表 -->
<div class="divide-y divide-gray-100">
<!-- 显示文件夹 -->
{% if folders and folders|length > 0 %}
{% for folder in folders %}
<div class="px-6 py-4 flex items-center justify-between transition-all duration-200 hover:file-item-hover">
<!-- 文件夹名 -->
<a href="{{ url_for('download_page', dir=current_dir ~ '\\' ~ folder if current_dir else folder) }}" class="flex items-center flex-1 mr-4" title="{{ folder }}">
<div class="w-10 h-10 rounded bg-blue-100 flex items-center justify-center mr-4 text-blue-500 flex-shrink-0">
<i class="fa fa-folder"></i>
</div>
<div class="overflow-x-auto whitespace-nowrap flex-1 max-w-xs sm:max-w-md scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<span class="font-medium text-gray-800 inline-block">{{ folder }}</span>
</div>
</a>
<!-- 进入按钮 -->
<a href="{{ url_for('download_page', dir=current_dir ~ '\\' ~ folder if current_dir else folder) }}"
class="inline-flex items-center bg-gray-100 text-gray-700 px-3 py-1 rounded-lg transition-all duration-200 hover:bg-gray-200">
<i class="fa fa-arrow-right mr-1"></i>
<span>进入</span>
</a>
</div>
{% endfor %}
{% endif %}
<!-- 显示文件 -->
{% if files and files|length > 0 %}
{% for file in files %}
<div class="px-6 py-4 flex items-center justify-between transition-all duration-200 hover:file-item-hover">
<!-- 文件名 -->
<div class="flex items-center flex-1 pr-4">
<div class="w-10 h-10 rounded bg-gray-100 flex items-center justify-center mr-4 text-gray-500 flex-shrink-0">
<i class="fa fa-file-o"></i>
</div>
<div class="overflow-x-auto whitespace-nowrap flex-1 max-w-xs sm:max-w-md scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent">
<span class="font-medium text-gray-800 inline-block" title="{{ file }}">{{ file }}</span>
</div>
</div>
<!-- 下载按钮 -->
<a href="{{ url_for('download_file', filename=file, dir=current_dir) }}"
class="inline-flex items-center bg-primary/10 text-primary px-4 py-2 rounded-lg transition-all duration-200 hover:bg-primary hover:text-white">
<i class="fa fa-download mr-2"></i>
<span>下载</span>
</a>
</div>
{% endfor %}
{% endif %}
<!-- 空状态 -->
{% if (not folders or folders|length == 0) and (not files or files|length == 0) %}
<div class="px-6 py-16 text-center">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 text-gray-400 mb-4">
<i class="fa fa-folder-open-o text-2xl"></i>
</div>
<p class="text-gray-500">当前目录为空</p>
</div>
{% endif %}
</div>
</div>
<!-- 返回按钮 -->
<div class="text-center">
<a href="{{ url_for('home') }}" class="inline-flex items-center text-primary hover:text-primary/80 transition-colors">
<i class="fa fa-arrow-left mr-2"></i>返回首页
</a>
</div>
<!-- 页脚 -->
<footer class="mt-16 text-center text-gray-400 text-sm">
<p>© 2025 FTP功能小网站 - 便捷快速的文件传输服务 - AndyDennis</p>
</footer>
</div>
<!-- 简单的动画效果 -->
<script>
// 添加页面加载动画
document.addEventListener('DOMContentLoaded', () => {
const fileItems = document.querySelectorAll('.divide-y > div');
fileItems.forEach((item, index) => {
setTimeout(() => {
item.classList.add('animate-fade-in');
}, 100 * index);
});
});
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
home.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FTP功能小网站</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 配置Tailwind自定义颜色和字体 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5', // 主色调:靛蓝色,传达信任和专业感
secondary: '#8B5CF6', // 辅助色:紫色
neutral: '#F8FAFC', // 中性色:浅灰
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.btn-hover {
@apply transform transition-all duration-300 hover:scale-105 hover:shadow-lg;
}
.card-effect {
@apply bg-white rounded-xl shadow-md transition-all duration-300 hover:shadow-xl;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-neutral to-blue-50 min-h-screen flex flex-col items-center justify-center p-4 font-sans text-gray-800">
<!-- 页面容器 -->
<div class="max-w-md w-full mx-auto text-center relative">
<!-- 标题区域 -->
<div class="mb-12 animate-fade-in">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<i class="fa fa-cloud-upload text-3xl"></i>
</div>
<h1 class="text-[clamp(2rem,5vw,3rem)] font-bold text-primary mb-2">FTP功能小网站</h1>
<p class="text-gray-500">简单高效的文件上传与下载服务</p>
</div>
<!-- 操作按钮区域 -->
<div class="grid gap-6">
<!-- 上传按钮 -->
<a href="{{ url_for('upload_file') }}" class="card-effect p-6 flex items-center justify-center space-x-3 btn-hover group">
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center text-green-600 group-hover:bg-green-600 group-hover:text-white transition-colors">
<i class="fa fa-upload"></i>
</div>
<span class="text-lg font-medium">上传文件</span>
</a>
<!-- 下载按钮 -->
<a href="{{ url_for('download_page') }}" class="card-effect p-6 flex items-center justify-center space-x-3 btn-hover group">
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<i class="fa fa-download"></i>
</div>
<span class="text-lg font-medium">下载文件</span>
</a>
</div>
<!-- 页脚 -->
<footer class="mt-16 text-gray-400 text-sm">
<p>© 2025 FTP功能小网站 - 便捷快速的文件传输服务 - AndyDennis</p>
</footer>
</div>
<!-- 简单的动画效果脚本 -->
<script>
// 添加页面加载动画类
document.addEventListener('DOMContentLoaded', () => {
const cards = document.querySelectorAll('.card-effect');
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('animate-slide-up');
}, 100 * index);
});
});
// 定义简单的CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out forwards;
}
.animate-slide-up {
animation: slideUp 0.5s ease-out forwards;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
login.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - FTP功能小网站</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 配置Tailwind自定义颜色和字体 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5', // 主色调:靛蓝色,传达信任和专业感
secondary: '#8B5CF6', // 辅助色:紫色
neutral: '#F8FAFC', // 中性色:浅灰
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.btn-hover {
@apply transform transition-all duration-300 hover:scale-105 hover:shadow-lg;
}
.card-effect {
@apply bg-white rounded-xl shadow-md transition-all duration-300 hover:shadow-xl;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-neutral to-blue-50 min-h-screen flex flex-col items-center justify-center p-4 font-sans text-gray-800">
<!-- 页面容器 -->
<div class="max-w-md w-full mx-auto text-center">
<!-- 标题区域 -->
<div class="mb-12 animate-fade-in">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4">
<i class="fa fa-lock text-3xl"></i>
</div>
<h1 class="text-[clamp(2rem,5vw,3rem)] font-bold text-primary mb-2">请输入密钥</h1>
<p class="text-gray-500">需要验证身份才能访问FTP功能</p>
</div>
<!-- 登录表单 -->
<div class="card-effect p-8 animate-slide-up">
{% if error %}
<div class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
{{ error }}
</div>
{% endif %}
<form method="POST" action="{{ url_for('login') }}">
<div class="mb-6">
<label for="key" class="block text-left text-gray-700 mb-2">密钥</label>
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-400">
<i class="fa fa-key"></i>
</span>
<input type="password" id="key" name="key"
class="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors"
placeholder="请输入访问密钥" required>
</div>
</div>
<button type="submit"
class="w-full bg-primary text-white py-3 px-6 rounded-lg font-medium btn-hover flex items-center justify-center space-x-2">
<i class="fa fa-sign-in"></i>
<span>验证密钥</span>
</button>
</form>
</div>
<!-- 页脚 -->
<footer class="mt-16 text-gray-400 text-sm">
<p>© 2025 FTP功能小网站 - 便捷快速的文件传输服务 - AndyDennis</p>
</footer>
</div>
<!-- 简单的动画效果脚本 -->
<script>
// 添加页面加载动画类
document.addEventListener('DOMContentLoaded', () => {
const cards = document.querySelectorAll('.card-effect');
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('animate-slide-up');
}, 100 * index);
});
});
// 定义简单的CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.6s ease-out forwards;
}
.animate-slide-up {
animation: slideUp 0.5s ease-out forwards;
}
`;
document.head.appendChild(style);
</script>
</body>
</html>
upload_success.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传成功 - FTP功能小网站</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 配置Tailwind自定义颜色 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5',
success: '#10B981', // 成功状态颜色
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.btn-action {
@apply px-6 py-3 rounded-lg font-medium transition-all duration-300 hover:shadow-lg transform hover:-translate-y-0.5;
}
.btn-primary {
@apply bg-primary text-white hover:bg-primary/90;
}
.btn-secondary {
@apply bg-gray-100 text-gray-700 hover:bg-gray-200;
}
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-teal-50 min-h-screen font-sans flex items-center justify-center p-4">
<div class="max-w-md w-full text-center relative">
<!-- 登出按钮 -->
<div class="absolute top-0 right-0 mt-2 mr-2">
<a href="{{ url_for('logout') }}" class="inline-flex items-center text-gray-500 hover:text-primary transition-colors px-3 py-2 rounded-lg hover:bg-primary/5">
<i class="fa fa-sign-out mr-1"></i>
<span>登出</span>
</a>
</div>
<!-- 成功状态卡片 -->
<div class="bg-white rounded-2xl shadow-lg p-8 md:p-10 transform transition-all duration-500 animate-float">
<!-- 成功图标 -->
<div class="w-20 h-20 mx-auto bg-success/10 rounded-full flex items-center justify-center mb-6 animate-pulse-slow">
<i class="fa fa-check text-4xl text-success"></i>
</div>
<!-- 成功信息 -->
<h1 class="text-[clamp(1.5rem,3vw,2rem)] font-bold text-gray-800 mb-3">上传成功!</h1>
{% if filename %}
{% if filename is iterable and filename is not string %}
<p class="text-gray-600 mb-8">
以下文件已成功上传:
</p>
<!-- 文件信息 -->
<div class="bg-gray-50 rounded-lg p-4 mb-8 text-left">
{% for file in filename %}
<div class="flex items-center text-sm text-gray-500 mb-2">
<i class="fa fa-file-text-o w-5 text-gray-400"></i>
<span class="ml-2 truncate">{{ file }}</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-gray-600 mb-8">
文件 <span class="font-medium text-primary">{{ filename }}</span> 已成功上传
</p>
<!-- 文件信息 -->
<div class="bg-gray-50 rounded-lg p-4 mb-8 text-left">
<div class="flex items-center text-sm text-gray-500">
<i class="fa fa-file-text-o w-5 text-gray-400"></i>
<span class="ml-2 truncate">{{ filename }}</span>
</div>
</div>
{% endif %}
{% else %}
<p class="text-gray-600 mb-8">
文件已成功上传
</p>
{% endif %}
<!-- 操作按钮 -->
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="{{ url_for('upload_file') }}" class="btn-action btn-primary flex items-center justify-center">
<i class="fa fa-upload mr-2"></i>上传另一个文件
</a>
<a href="{{ url_for('home') }}" class="btn-action btn-secondary flex items-center justify-center">
<i class="fa fa-home mr-2"></i>返回首页
</a>
</div>
</div>
<!-- 页脚 -->
<footer class="mt-10 text-gray-400 text-sm">
<p>© 2025 FTP功能小网站 - 便捷快速的文件传输服务 - AndyDennis</p>
</footer>
</div>
<!-- 动画样式 -->
<style>
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes pulseSlow {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
.animate-pulse-slow {
animation: pulseSlow 2s ease-in-out infinite;
}
</style>
</body>
</html>
upload.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>上传文件 - FTP功能小网站</title>
<!-- 引入Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入Font Awesome -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- 引入Dropzone.js CSS -->
<link rel="stylesheet" href="https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" type="text/css" />
<!-- 配置Tailwind CSS -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#4F46E5',
secondary: '#8B5CF6',
accent: '#10B981', // 上传相关的强调色(绿色)
}
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.file-upload-hover {
@apply border-accent bg-accent/5;
}
.btn-primary {
@apply bg-primary text-white px-6 py-3 rounded-lg font-medium transition-all duration-300 hover:bg-primary/90 hover:shadow-lg transform hover:-translate-y-0.5;
}
.card {
@apply bg-white rounded-xl shadow-md p-6 md:p-8 transition-all duration-300 hover:shadow-xl;
}
}
</style>
<!-- Dropzone自定义样式 -->
<style>
/* 上传区域样式 */
.dropzone {
border: 2px dashed #e5e7eb !important;
border-radius: 0.5rem !important;
background-color: #ffffff !important;
transition: all 0.3s ease !important;
}
.dropzone:hover,
.dropzone.dz-clickable:hover {
border-color: #10B981 !important;
background-color: rgba(16, 185, 129, 0.05) !important;
}
/* 上传区域文字样式 */
.dz-message {
margin: 2rem 0 !important;
text-align: center !important;
}
/* 文件预览样式 */
.dz-preview {
margin: 0.5rem !important;
border: 1px solid #e5e7eb !important;
border-radius: 0.375rem !important;
background-color: #f9fafb !important;
padding: 0.75rem !important;
transition: all 0.2s ease !important;
}
.dz-preview:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
}
/* 文件信息样式 */
.dz-details {
margin-bottom: 0.5rem !important;
}
.dz-filename {
font-size: 0.875rem !important;
font-weight: 500 !important;
color: #374151 !important;
}
.dz-size {
font-size: 0.75rem !important;
color: #6b7280 !important;
margin-top: 0.25rem !important;
}
/* 进度条样式 */
.dz-progress {
background-color: #e5e7eb !important;
border-radius: 9999px !important;
overflow: hidden !important;
height: 0.5rem !important;
margin: 0.5rem 0 !important;
}
.dz-upload {
background-color: #10B981 !important;
height: 100% !important;
border-radius: 9999px !important;
transition: width 0.15s ease !important;
}
/* 成功/错误标记样式 */
.dz-success-mark,
.dz-error-mark {
position: absolute !important;
top: 0.5rem !important;
right: 0.5rem !important;
font-size: 1rem !important;
}
.dz-success-mark {
color: #10B981 !important;
}
.dz-error-mark {
color: #ef4444 !important;
}
/* 错误消息样式 */
.dz-error-message {
font-size: 0.75rem !important;
color: #ef4444 !important;
margin-top: 0.25rem !important;
padding: 0.25rem !important;
background-color: rgba(239, 68, 68, 0.1) !important;
border-radius: 0.25rem !important;
}
/* 移除文件按钮样式 */
.dz-preview .dz-remove {
font-size: 0.75rem !important;
color: #6366f1 !important;
text-decoration: none !important;
margin-top: 0.5rem !important;
display: inline-block !important;
transition: color 0.2s ease !important;
}
.dz-preview .dz-remove:hover {
color: #4f46e5 !important;
text-decoration: underline !important;
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-teal-50 min-h-screen font-sans">
<div class="container mx-auto px-4 py-8 max-w-3xl relative">
<!-- 登出按钮 -->
<div class="absolute top-0 right-0 mt-2 mr-2">
<a href="{{ url_for('logout') }}" class="inline-flex items-center text-gray-500 hover:text-primary transition-colors px-3 py-2 rounded-lg hover:bg-primary/5">
<i class="fa fa-sign-out mr-1"></i>
<span>登出</span>
</a>
</div>
<!-- 页面标题 -->
<header class="text-center mb-10 mt-6">
<h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-primary mb-2">
<i class="fa fa-cloud-upload mr-3"></i>上传文件
</h1>
<p class="text-gray-500">选择并上传您需要存储的文件</p>
</header>
<!-- 上传卡片 -->
<div class="card mb-8">
<!-- Dropzone上传区域 -->
<form action="{{ url_for('upload_success')}}" method="POST" enctype="multipart/form-data" class="dropzone mb-6" id="my-dropzone">
<div class="dz-message">
<i class="fa fa-file-o text-5xl text-gray-400 mb-4"></i>
<p class="text-gray-600 mb-4">拖放文件到此处,或</p>
<button type="button" class="btn-primary inline-flex items-center">
<i class="fa fa-folder-open mr-2"></i>选择文件
</button>
</div>
</form>
<!-- 上传说明 -->
<div class="text-sm text-gray-500 bg-gray-50 p-4 rounded-lg">
<p><i class="fa fa-info-circle text-primary mr-2"></i>上传说明:</p>
<ul class="list-disc list-inside mt-2 space-y-1">
<li>请确保文件符合上传规范</li>
<li>支持多种文件格式上传</li>
<li>上传完成后将自动跳转至成功页面</li>
</ul>
</div>
</div>
<!-- 返回按钮 -->
<div class="text-center">
<a href="{{ url_for('home') }}" class="inline-flex items-center text-primary hover:text-primary/80 transition-colors">
<i class="fa fa-arrow-left mr-2"></i>返回首页
</a>
</div>
<!-- 页脚 -->
<footer class="mt-16 text-center text-gray-400 text-sm">
<p>© 2025 FTP功能小网站 - 便捷快速的文件传输服务 - AndyDennis</p>
</footer>
</div>
<!-- 引入Dropzone.js JS -->
<script src="https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"></script>
<!-- Dropzone配置脚本 -->
<script>
console.log("开始初始化Dropzone.js");
// 确保DOM加载完成
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM加载完成,初始化Dropzone");
// 配置Dropzone
Dropzone.options.myDropzone = {
paramName: "file", // 与后端接收的参数名一致
maxFilesize: 1024*8, // 文件大小限制,单位MB
maxFiles: 10, // 最大上传文件数
autoProcessQueue: true, // 自动处理上传队列
uploadMultiple: true, // 批量发送多个文件
parallelUploads: 5, // 并行上传数
acceptedFiles: "*/*", // 接受所有文件类型
dictDefaultMessage: "", // 清空默认消息,使用自定义消息
dictFallbackMessage: "您的浏览器不支持拖放文件上传功能。",
dictFileTooBig: "文件过大 ({{filesize}}MB)。最大文件大小: {{maxFilesize}}MB。",
dictInvalidFileType: "您不能上传这种类型的文件。",
dictResponseError: "服务器响应错误 ({{statusCode}})。",
dictCancelUpload: "取消上传",
dictCancelUploadConfirmation: "确定要取消上传吗?",
dictRemoveFile: "移除文件",
dictRemoveFileConfirmation: null,
dictMaxFilesExceeded: "您最多只能上传 {{maxFiles}} 个文件。",
// 自定义样式
previewTemplate: `
<div class="dz-preview dz-file-preview">
<div class="dz-details">
<div class="dz-filename"><span data-dz-name></span></div>
<div class="dz-size" data-dz-size></div>
</div>
<div class="dz-progress" style="display: block;"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-times"></i></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
</div>
`,
// 初始化完成回调
init: function() {
console.log("Dropzone初始化完成");
// 监听文件添加事件
this.on("addedfile", function(file) {
console.log("添加文件:", file.name);
});
// 监听队列完成事件
this.on("queuecomplete", function() {
console.log("队列上传完成");
window.location.href = "{{ url_for('upload_success') }}";
});
},
// 上传成功后的回调
success: function(file, response) {
console.log("上传成功:", file.name);
console.log("服务器响应:", response);
},
// 上传完成后的回调
complete: function(file) {
console.log("上传完成:", file.name);
},
// 上传错误的回调
error: function(file, errorMessage) {
console.error("上传错误:", errorMessage);
},
// 上传进度回调
uploading: function(file, progress, bytesSent) {
console.log("上传中:", file.name, "进度:", progress + "%");
}
};
console.log("Dropzone配置完成");
});
</script>
</body>
</html>
app.py
后端,可以同时接受多个文件。
python
from flask import Flask, render_template, request, send_from_directory, abort, redirect, url_for, session
import os
import secrets
import time
import functools
app = Flask(__name__)
app.secret_key = secrets.token_hex(16) # 用于加密session
# 定义访问密钥
ACCESS_KEY = "12345678" # 可以根据实际需求修改
# token有效期(秒)
TOKEN_EXPIRY = 3600 # 1小时
def getfile():
print(os.getcwd())
path1 = os.getcwd() + '/upload_files'
f_list = []
# 递归遍历所有子文件夹,获取所有文件
for root, dirs, files in os.walk(path1):
for file in files:
f_list.append(file)
print(os.getcwd())
print(f"获取到的文件列表: {f_list}")
return f_list
def is_Have_file(filename):
print(os.getcwd())
path1 = os.getcwd() + '/upload_files'
# 递归遍历所有子文件夹,检查文件是否存在
for root, dirs, files in os.walk(path1):
if filename in files:
print(f"文件{filename}存在于{root}目录中")
return True
print(f"文件{filename}不存在")
return False
# 登录装饰器,用于检查用户是否已登录
def login_required(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
# 检查session中是否有token和token过期时间
if 'token' not in session or 'token_expiry' not in session:
return redirect(url_for('login'))
# 检查token是否过期
if time.time() > session['token_expiry']:
# token过期,清除session并跳转到登录页
session.pop('token', None)
session.pop('token_expiry', None)
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# 登录路由
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 获取用户输入的密钥
key = request.form.get('key')
# 验证密钥
if key == ACCESS_KEY:
# 生成token并保存到session
session['token'] = secrets.token_hex(32)
session['token_expiry'] = time.time() + TOKEN_EXPIRY
# 重定向到首页
return redirect(url_for('home'))
else:
# 密钥错误,显示错误信息
return render_template('login.html', error='密钥错误,请重新输入')
# GET请求,显示登录页
return render_template('login.html')
# 登出路由
@app.route('/logout')
def logout():
# 清除session中的token信息
session.pop('token', None)
session.pop('token_expiry', None)
return redirect(url_for('login'))
@app.route('/')
@login_required
def home():
return render_template('home.html')
@app.route('/upload')
@login_required
def upload_file():
return render_template('upload.html')
@app.route('/uploader', methods=['GET', 'POST'])
@login_required
def upload_success():
if request.method == 'POST':
print("接收到POST请求")
print("request.files:", request.files)
# 处理多文件上传
files = []
if 'file' in request.files:
print("'file'在request.files中")
files = request.files.getlist('file') # 获取多个文件
elif 'file[]' in request.files:
print("'file[]'在request.files中")
files = request.files.getlist('file[]') # 获取多个文件(Dropzone.js默认参数名)
if files:
print("获取到的文件数量:", len(files))
uploaded_files = []
for i, file in enumerate(files):
print(f"文件{i}: {file.filename}")
if file.filename:
try:
# 确保upload_files目录存在
if not os.path.exists('upload_files'):
os.makedirs('upload_files')
print("创建了upload_files目录")
# 获取文件扩展名
file_extension = os.path.splitext(file.filename)[1].lower().lstrip('.')
# 如果没有扩展名,使用'other'文件夹
if not file_extension:
file_extension = 'other'
# 创建对应扩展名的文件夹
extension_folder = os.path.join('upload_files', file_extension)
if not os.path.exists(extension_folder):
os.makedirs(extension_folder)
print(f"创建了{file_extension}文件夹")
# 保存文件到对应扩展名的文件夹
file_path = os.path.join(extension_folder, file.filename)
file.save(file_path)
print(f"文件保存成功: {file_path}")
uploaded_files.append(file.filename)
except Exception as e:
print(f"保存文件时出错: {str(e)}")
print(f"成功上传的文件: {uploaded_files}")
return render_template('upload_success.html', filename=uploaded_files)
else:
print("'file'不在request.files中")
# 处理GET请求
print("处理GET请求")
return render_template('upload_success.html')
# 显示下载文件的界面
@app.route('/down', methods=['GET'])
@login_required
def download_page():
# 获取当前目录参数,默认为空(表示根目录upload_files)
current_dir = request.args.get('dir', '')
# 构建完整的目录路径
base_dir = 'upload_files'
full_path = os.path.join(base_dir, current_dir)
# 检查路径是否存在且是目录
if not os.path.exists(full_path) or not os.path.isdir(full_path):
# 如果路径不存在,返回根目录内容
current_dir = ''
full_path = base_dir
# 获取当前目录下的文件夹和文件
folders = []
files = []
if os.path.exists(full_path):
for item in os.listdir(full_path):
item_path = os.path.join(full_path, item)
if os.path.isdir(item_path):
folders.append(item)
else:
files.append(item)
# 构建面包屑导航
breadcrumbs = []
if current_dir:
# 分割当前目录路径(同时处理/和\作为分隔符)
path_parts = current_dir.replace('/', '\\').split('\\')
# 移除空部分
path_parts = [part for part in path_parts if part]
# 构建每个层级的面包屑
for i, part in enumerate(path_parts):
# 构建到当前层级的路径
crumb_path = '\\'.join(path_parts[:i+1])
breadcrumbs.append({'name': part, 'path': crumb_path})
print(f"当前目录: {current_dir}")
print(f"完整路径: {full_path}")
print(f"文件夹: {folders}")
print(f"文件: {files}")
return render_template('download_page.html',
folders=folders,
files=files,
current_dir=current_dir,
breadcrumbs=breadcrumbs)
# 下载要下载的文件,要下载的文件是通过get方法来传递的
@app.route('/download_file', methods=['GET'])
@login_required
def download_file():
if request.method == 'GET':
download_filename = request.args.get('filename')
current_dir = request.args.get('dir', '')
# 构建文件路径
base_dir = 'upload_files'
if current_dir:
file_path = os.path.join(base_dir, current_dir, download_filename)
else:
file_path = os.path.join(base_dir, download_filename)
# 检查文件是否存在
if os.path.exists(file_path) and os.path.isfile(file_path):
# 确定文件所在的目录
file_dir = os.path.dirname(file_path)
# 从文件所在的目录发送文件
return send_from_directory(file_dir, download_filename, as_attachment=True)
else:
# 如果直接路径找不到,尝试在所有子目录中查找
path1 = os.getcwd() + '/upload_files'
for root, dirs, files in os.walk(path1):
if download_filename in files:
# 从文件所在的目录发送文件
return send_from_directory(root, download_filename, as_attachment=True)
# 如果找不到文件
abort(404)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)