first commit -push
This commit is contained in:
12
RobotNet.SystemUpgrade/AppJsonSerializerContext.cs
Normal file
12
RobotNet.SystemUpgrade/AppJsonSerializerContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RobotNet.SystemUpgrade;
|
||||
|
||||
public sealed record UploadFileResponse(string Name, long Size, string Url);
|
||||
public sealed record FileItem(string Name, long Size, DateTime CreatedUtc, string Url);
|
||||
|
||||
[JsonSerializable(typeof(UploadFileResponse))]
|
||||
[JsonSerializable(typeof(FileItem))]
|
||||
internal partial class AppJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
105
RobotNet.SystemUpgrade/Program.cs
Normal file
105
RobotNet.SystemUpgrade/Program.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using RobotNet.SystemUpgrade;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
|
||||
});
|
||||
|
||||
builder.WebHost.ConfigureKestrel(o =>
|
||||
{
|
||||
o.Limits.MaxRequestBodySize = 2L * 1024 * 1024 * 1024;
|
||||
});
|
||||
|
||||
builder.Services.Configure<FormOptions>(o =>
|
||||
{
|
||||
o.MultipartBodyLengthLimit = 2L * 1024 * 1024 * 1024;
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
var storeDir = Path.Combine(AppContext.BaseDirectory, "store");
|
||||
Directory.CreateDirectory(storeDir);
|
||||
|
||||
|
||||
var apis = app.MapGroup("/api");
|
||||
|
||||
|
||||
var upload = apis.MapPost("/upload", async (HttpRequest request) =>
|
||||
{
|
||||
if (!request.HasFormContentType)
|
||||
return Results.BadRequest("Content-Type phải là multipart/form-data.");
|
||||
|
||||
var form = await request.ReadFormAsync();
|
||||
var file = form.Files.FirstOrDefault();
|
||||
if (file is null || file.Length == 0)
|
||||
return Results.BadRequest("Không có file hoặc file rỗng.");
|
||||
|
||||
var safeName = Path.GetFileName(file.FileName);
|
||||
var savePath = Path.Combine(storeDir, safeName);
|
||||
|
||||
await using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 64 * 1024, useAsync: true))
|
||||
await file.CopyToAsync(fs);
|
||||
var nowUtc = DateTime.UtcNow;
|
||||
try
|
||||
{
|
||||
System.IO.File.SetCreationTimeUtc(savePath, nowUtc);
|
||||
System.IO.File.SetLastWriteTimeUtc(savePath, nowUtc);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
var fi = new FileInfo(savePath);
|
||||
var url = $"/api/files/{Uri.EscapeDataString(fi.Name)}";
|
||||
return Results.Created(url, new UploadFileResponse(fi.Name, fi.Length, url));
|
||||
});
|
||||
|
||||
|
||||
apis.MapGet("/files", () =>
|
||||
{
|
||||
var list = Directory.EnumerateFiles(storeDir)
|
||||
.Select(p => new FileInfo(p))
|
||||
.OrderByDescending(f => f.CreationTimeUtc)
|
||||
.Select(f => new FileItem(
|
||||
f.Name, f.Length, f.CreationTimeUtc,
|
||||
$"/api/files/{Uri.EscapeDataString(f.Name)}"
|
||||
))
|
||||
.ToList();
|
||||
|
||||
return Results.Ok(list);
|
||||
});
|
||||
|
||||
|
||||
apis.MapGet("/files/{name}", (string name) =>
|
||||
{
|
||||
var safeName = Path.GetFileName(name);
|
||||
var path = Path.Combine(storeDir, safeName);
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Results.NotFound("Không tìm thấy file.");
|
||||
|
||||
return Results.File(path, "application/octet-stream", fileDownloadName: safeName);
|
||||
});
|
||||
|
||||
|
||||
apis.MapDelete("/files/{name}", (string name) =>
|
||||
{
|
||||
var safeName = Path.GetFileName(name);
|
||||
var path = Path.Combine(storeDir, safeName);
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Results.NotFound("Không tìm thấy file.");
|
||||
|
||||
System.IO.File.Delete(path);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseDefaultFiles();
|
||||
app.UseRouting();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
app.Run();
|
||||
16
RobotNet.SystemUpgrade/Properties/launchSettings.json
Normal file
16
RobotNet.SystemUpgrade/Properties/launchSettings.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "index.html",
|
||||
"workingDirectory": "$(TargetDir)",
|
||||
"applicationUrl": "http://localhost:5296",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj
Normal file
11
RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj
Normal file
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
25
RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln
Normal file
25
RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36401.2 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.SystemUpgrade", "RobotNet.SystemUpgrade.csproj", "{D1EF28B0-A055-A148-A814-787BB9EC1F74}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{D1EF28B0-A055-A148-A814-787BB9EC1F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D1EF28B0-A055-A148-A814-787BB9EC1F74}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D1EF28B0-A055-A148-A814-787BB9EC1F74}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D1EF28B0-A055-A148-A814-787BB9EC1F74}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {65A7E842-E9B6-4B96-A47E-C20BC331568D}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
8
RobotNet.SystemUpgrade/appsettings.Development.json
Normal file
8
RobotNet.SystemUpgrade/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
RobotNet.SystemUpgrade/appsettings.json
Normal file
9
RobotNet.SystemUpgrade/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
15
RobotNet.SystemUpgrade/libman.json
Normal file
15
RobotNet.SystemUpgrade/libman.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "3.0",
|
||||
"defaultProvider": "cdnjs",
|
||||
"libraries": [
|
||||
{
|
||||
"library": "bootstrap@5.3.7",
|
||||
"destination": "wwwroot/lib/bootstrap/"
|
||||
},
|
||||
{
|
||||
"provider": "jsdelivr",
|
||||
"library": "@mdi/font@7.4.47",
|
||||
"destination": "wwwroot/lib/mdi/font/"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
RobotNet.SystemUpgrade/readme.md
Normal file
1
RobotNet.SystemUpgrade/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
dotnet publish -c Release -r linux-x64 --self-contained true -o ./bin/Release/net9.0/publish
|
||||
5
RobotNet.SystemUpgrade/wwwroot/favicon.svg
Normal file
5
RobotNet.SystemUpgrade/wwwroot/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<path d="M31.81,1.79,21.29,14.32l-2.48-3a2.6,2.6,0,0,1,0-3.34l4.45-5.31a2.64,2.64,0,0,1,2-.93Z" fill="#e06e2e"/>
|
||||
<path d="M.19,1.8,10.71,14.34l2.48-2.95a2.6,2.6,0,0,0,0-3.34L8.75,2.74a2.66,2.66,0,0,0-2-.94Z" fill="#e06e2e"/>
|
||||
<path d="M32,30.21H25.36a2.61,2.61,0,0,1-2-.92L16.5,21.1a.65.65,0,0,0-1,0L8.63,29.29a2.58,2.58,0,0,1-2,.92H0L12.07,15.83,14,13.57a2.67,2.67,0,0,1,4.08,0l1.89,2.26Z" fill="#233871"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
553
RobotNet.SystemUpgrade/wwwroot/index.html
Normal file
553
RobotNet.SystemUpgrade/wwwroot/index.html
Normal file
@@ -0,0 +1,553 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>File Upload Manager</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="lib/mdi/font/css/materialdesignicons.min.css" />
|
||||
<style>
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
mdi {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-size: cover;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
color: #e2e8f0;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial;
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100svh;
|
||||
display: grid;
|
||||
grid-template-rows: 20% 80%;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
text-align:center;
|
||||
background: rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 36px rgba(0,0,0,.28);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.upload-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-drop-zone {
|
||||
flex: 1 1 auto;
|
||||
border: 2px dashed rgba(56,189,248,.5);
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
background: rgba(56,189,248,.06);
|
||||
transition: .2s ease;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-drop-zone.dragover {
|
||||
border-color: #0ea5e9;
|
||||
background: rgba(14,165,233,.15);
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.file-input-wrapper input[type=file] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input-label {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
padding: .5rem .9rem;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg,#0ea5e9,#38bdf8);
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
color: #cbd5e1;
|
||||
max-width: 40vw;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-upload {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: .6rem 1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg,#10b981,#059669);
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 0;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.12);
|
||||
overflow: hidden;
|
||||
flex: 0 0 240px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
background: linear-gradient(90deg,#10b981,#059669);
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
|
||||
.files-section {
|
||||
background: rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 18px 36px rgba(0,0,0,.28);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.files-header {
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg,#8b5cf6,#9333ea);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.files-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
background: rgba(255,255,255,0.02);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.pagination-btn {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: #e2e8f0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.pagination-btn:hover:not(:disabled) {
|
||||
background: rgba(139, 92, 246, 0.3);
|
||||
border-color: rgba(139, 92, 246, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
color: #94a3b8;
|
||||
font-size: 0.9rem;
|
||||
margin: 0 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: rgba(255,255,255,.05);
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: rgba(255,255,255,.08);
|
||||
border-color: rgba(139, 92, 246, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #e2e8f0;
|
||||
margin: 0;
|
||||
word-break: break-word;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #94a3b8;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.btn-file {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: .45rem .8rem;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-file:hover {
|
||||
transform: translateY(-1px);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: linear-gradient(135deg,#0ea5e9,#38bdf8);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: linear-gradient(135deg,#ef4444,#dc2626);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.upload-form {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.progress {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.selected-file {
|
||||
max-width: 60vw;
|
||||
display: block;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
order: -1;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
|
||||
<section class="upload-section">
|
||||
<form id="uploadForm" class="upload-form" onsubmit="return false">
|
||||
<div class="file-drop-zone" id="dropZone">
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" id="fileInput" />
|
||||
<label for="fileInput" class="file-input-label"> <i class="mdi mdi-folder-upload "></i> Chọn tệp</label>
|
||||
</div>
|
||||
<span id="selectedFile" class="selected-file" style="display:none"></span>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="uploadBtn" class="btn btn-upload" type="submit" disabled> <i class="mdi mdi-rocket "></i> Upload</button>
|
||||
<div id="progressContainer" class="progress">
|
||||
<div id="uploadProgress" class="progress-bar" style="width:0%">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="files-section">
|
||||
<div class="files-header">
|
||||
<h5 class="m-0"> Uploaded Files</h5>
|
||||
<div class="badge bg-light text-dark" id="fileCount">0 files</div>
|
||||
</div>
|
||||
<div id="filesList" class="files-body">
|
||||
<div class="loading-state">Loading files…</div>
|
||||
</div>
|
||||
<div class="pagination-container" id="paginationContainer" style="display: none;">
|
||||
<button class="pagination-btn" id="prevBtn"><i class="mdi mdi-arrow-left "></i> Previous</button>
|
||||
<span class="pagination-info" id="pageInfo">Page 1 of 1</span>
|
||||
<button class="pagination-btn" id="nextBtn">Next <i class="mdi mdi-arrow-right"></i></button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const $ = s => document.querySelector(s);
|
||||
const fileInput = $("#fileInput");
|
||||
const selectedFile = $("#selectedFile");
|
||||
const uploadBtn = $("#uploadBtn");
|
||||
const progressContainer = $("#progressContainer");
|
||||
const progressBar = $("#uploadProgress");
|
||||
const dropZone = $("#dropZone");
|
||||
const fileCount = $("#fileCount");
|
||||
|
||||
|
||||
let currentPage = 1;
|
||||
let filesPerPage = 7;
|
||||
let allFiles = [];
|
||||
|
||||
const setProgress = p => { progressBar.style.width = p + "%"; progressBar.textContent = Math.floor(p) + "%"; };
|
||||
|
||||
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(allFiles.length / filesPerPage);
|
||||
const pageInfo = $("#pageInfo");
|
||||
const prevBtn = $("#prevBtn");
|
||||
const nextBtn = $("#nextBtn");
|
||||
const paginationContainer = $("#paginationContainer");
|
||||
|
||||
if (allFiles.length > filesPerPage) {
|
||||
paginationContainer.style.display = 'flex';
|
||||
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`;
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
} else {
|
||||
paginationContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function renderCurrentPage() {
|
||||
const list = $("#filesList");
|
||||
const startIndex = (currentPage - 1) * filesPerPage;
|
||||
const endIndex = startIndex + filesPerPage;
|
||||
const currentFiles = allFiles.slice(startIndex, endIndex);
|
||||
|
||||
if (allFiles.length === 0) {
|
||||
list.innerHTML = `<div class="empty-state">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"><i class="mdi mdi-folder"></i></div>
|
||||
<h5>Chưa có tệp nào</h5>
|
||||
<p>Tải lên tệp đầu tiên của bạn bằng form ở trên</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = "";
|
||||
currentFiles.forEach(f => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "file-item";
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<p class="file-name mb-1"><i class="mdi mdi-file-document "></i> ${f.name}</p>
|
||||
<div class="file-size">${(f.size / 1024).toFixed(1)} KB${f.createdUtc ? ' • ' + new Date(f.createdUtc).toLocaleString() : ''}</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn-file btn-download" href="${f.url}"> <i class="mdi mdi-briefcase-download "></i>Tải xuống</a>
|
||||
<button class="btn-file btn-delete" data-name="${f.name}"><i class="mdi mdi-delete-forever "></i> Xoá</button>
|
||||
</div>`;
|
||||
|
||||
const deleteBtn = row.querySelector(".btn-delete");
|
||||
deleteBtn.addEventListener("click", async () => {
|
||||
if (!confirm(`Xoá "${f.name}"?`)) return;
|
||||
|
||||
deleteBtn.disabled = true;
|
||||
deleteBtn.innerHTML = "⏳ Đang xoá...";
|
||||
|
||||
try {
|
||||
const del = await fetch(`/api/files/${encodeURIComponent(f.name)}`, { method: "DELETE" });
|
||||
if (del.ok) {
|
||||
row.style.opacity = '0';
|
||||
row.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => loadFiles(), 300);
|
||||
} else {
|
||||
throw new Error('Delete failed');
|
||||
}
|
||||
} catch {
|
||||
alert("Xoá thất bại");
|
||||
deleteBtn.disabled = false;
|
||||
deleteBtn.innerHTML = " Xoá";
|
||||
}
|
||||
});
|
||||
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$("#prevBtn").addEventListener("click", () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderCurrentPage();
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
$("#nextBtn").addEventListener("click", () => {
|
||||
const totalPages = Math.ceil(allFiles.length / filesPerPage);
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderCurrentPage();
|
||||
updatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
if (fileInput.files.length) {
|
||||
const f = fileInput.files[0];
|
||||
selectedFile.style.display = "inline-block";
|
||||
selectedFile.textContent = ` ${f.name} (${(f.size / 1024).toFixed(1)} KB)`;
|
||||
uploadBtn.disabled = false;
|
||||
} else {
|
||||
selectedFile.style.display = "none";
|
||||
selectedFile.textContent = "";
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("dragover"); });
|
||||
dropZone.addEventListener("dragleave", e => { e.preventDefault(); dropZone.classList.remove("dragover"); });
|
||||
dropZone.addEventListener("drop", e => {
|
||||
e.preventDefault(); dropZone.classList.remove("dragover");
|
||||
if (e.dataTransfer.files.length) {
|
||||
fileInput.files = e.dataTransfer.files;
|
||||
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
});
|
||||
|
||||
async function loadFiles() {
|
||||
const list = $("#filesList");
|
||||
list.innerHTML = `<div class="loading-state">Loading files…</div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/files");
|
||||
const files = await res.json();
|
||||
|
||||
allFiles = Array.isArray(files) ? files : [];
|
||||
currentPage = 1;
|
||||
|
||||
fileCount.textContent = `${allFiles.length} file${allFiles.length !== 1 ? 's' : ''}`;
|
||||
|
||||
renderCurrentPage();
|
||||
updatePagination();
|
||||
|
||||
} catch {
|
||||
list.innerHTML = `<div class="empty-state text-danger">
|
||||
<div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;">⚠️</div>
|
||||
<h5>Không tải được danh sách</h5>
|
||||
<p>Vui lòng thử lại sau</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
$("#uploadForm").addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
if (!fileInput.files.length) return;
|
||||
|
||||
const file = fileInput.files[0];
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = "⏳ Đang tải lên...";
|
||||
progressContainer.style.display = "block";
|
||||
setProgress(0);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/upload");
|
||||
xhr.upload.onprogress = evt => { if (evt.lengthComputable) setProgress((evt.loaded / evt.total) * 100); };
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
setProgress(100);
|
||||
setTimeout(() => {
|
||||
fileInput.value = "";
|
||||
fileInput.dispatchEvent(new Event("change"));
|
||||
progressContainer.style.display = "none";
|
||||
setProgress(0);
|
||||
uploadBtn.innerHTML = " Upload";
|
||||
loadFiles();
|
||||
}, 800);
|
||||
} else {
|
||||
alert("Upload thất bại");
|
||||
resetUpload();
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
alert("Lỗi mạng");
|
||||
resetUpload();
|
||||
};
|
||||
|
||||
function resetUpload() {
|
||||
progressContainer.style.display = "none";
|
||||
setProgress(0);
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = " Upload";
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
xhr.send(form);
|
||||
});
|
||||
|
||||
loadFiles();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user