chore: initial commit
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using WinSCP;
|
||||
|
||||
namespace WebApi
|
||||
{
|
||||
[RoutePrefix("api/craftfile")]
|
||||
public class CraftFileController : ApiController
|
||||
{
|
||||
// 本地缓存根目录
|
||||
private readonly string _cacheRoot = @"D:\\CraftFilesCache";
|
||||
|
||||
private readonly string _sftpHost = "10.107.69.5";
|
||||
private readonly string _sftpUser = "root";
|
||||
private readonly string _sftpPassword = "XKYXmes@123!";
|
||||
|
||||
public class FileRequestDto
|
||||
{
|
||||
// 远程(SFTP)相对路径或绝对路径:如 /root/PLM-MES/.../xxx.pdf
|
||||
public string RelativePath { get; set; } = "";
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("browse")]
|
||||
public IHttpActionResult Browse([FromBody] FileRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.RelativePath))
|
||||
return Ok(new { success = false, message = "路径不能为空" });
|
||||
|
||||
EnsureCacheRoot();
|
||||
|
||||
var remotePath = NormalizeRemotePath(request.RelativePath);
|
||||
var localPath = ToLocalPath(remotePath);
|
||||
|
||||
var isFile = Path.HasExtension(remotePath);
|
||||
|
||||
if (isFile)
|
||||
{
|
||||
// 确保本地存在
|
||||
if (!System.IO.File.Exists(localPath))
|
||||
{
|
||||
var ok = DownloadFileFromSftp(remotePath, localPath, out string err);
|
||||
if (!ok) return Ok(new { success = false, message = $"下载失败: {err}" });
|
||||
}
|
||||
return GetFilePreviewLocal(localPath, remotePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 目录:优先读取本地,没有则读取远程目录结构
|
||||
if (Directory.Exists(localPath))
|
||||
{
|
||||
return GetDirectoryContentsLocal(localPath, remotePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetDirectoryContentsRemote(remotePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new { success = false, message = $"服务器错误: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("download")]
|
||||
public IHttpActionResult Download([FromBody] FileRequestDto request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.RelativePath))
|
||||
return Ok(new { success = false, message = "文件路径不能为空" });
|
||||
|
||||
EnsureCacheRoot();
|
||||
|
||||
var remotePath = NormalizeRemotePath(request.RelativePath);
|
||||
var localPath = ToLocalPath(remotePath);
|
||||
|
||||
if (!Path.HasExtension(remotePath))
|
||||
return Ok(new { success = false, message = "请传入文件路径" });
|
||||
|
||||
if (!System.IO.File.Exists(localPath))
|
||||
{
|
||||
var ok = DownloadFileFromSftp(remotePath, localPath, out string err);
|
||||
if (!ok) return Ok(new { success = false, message = $"下载失败: {err}" });
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
var contentType = GetContentType(fileInfo.Name);
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StreamContent(new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
};
|
||||
response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
||||
response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
|
||||
{
|
||||
FileName = fileInfo.Name
|
||||
};
|
||||
return ResponseMessage(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new { success = false, message = $"下载文件失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:浏览器直接预览文件(支持PDF等),支持Range分块
|
||||
[HttpGet]
|
||||
[Route("preview")]
|
||||
public HttpResponseMessage Preview([FromUri] string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.BadRequest, new { success = false, message = "文件路径不能为空" });
|
||||
}
|
||||
|
||||
EnsureCacheRoot();
|
||||
var remotePath = NormalizeRemotePath(path);
|
||||
var localPath = ToLocalPath(remotePath);
|
||||
|
||||
if (!Path.HasExtension(remotePath))
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.BadRequest, new { success = false, message = "请传入文件路径" });
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(localPath))
|
||||
{
|
||||
var ok = DownloadFileFromSftp(remotePath, localPath, out string err);
|
||||
if (!ok)
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.InternalServerError, new { success = false, message = $"下载失败: {err}" });
|
||||
}
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
var contentType = GetContentType(fileInfo.Name);
|
||||
var totalLength = fileInfo.Length;
|
||||
|
||||
var response = new HttpResponseMessage();
|
||||
var range = Request.Headers.Range;
|
||||
|
||||
var stream = new FileStream(localPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
if (range != null && range.Ranges.Count > 0 && totalLength > 0)
|
||||
{
|
||||
// 处理 Range 请求
|
||||
var from = range.Ranges.First().From ?? 0;
|
||||
var to = range.Ranges.First().To ?? (totalLength - 1);
|
||||
if (to >= totalLength) to = totalLength - 1;
|
||||
var length = to - from + 1;
|
||||
stream.Seek(from, SeekOrigin.Begin);
|
||||
|
||||
response.StatusCode = HttpStatusCode.PartialContent;
|
||||
response.Content = new StreamContent(stream);
|
||||
response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
response.Content.Headers.ContentLength = length;
|
||||
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(from, to, totalLength);
|
||||
response.Headers.AcceptRanges.Add("bytes");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 全量流式
|
||||
response.StatusCode = HttpStatusCode.OK;
|
||||
response.Content = new StreamContent(stream);
|
||||
response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
response.Content.Headers.ContentLength = totalLength;
|
||||
response.Headers.AcceptRanges.Add("bytes");
|
||||
}
|
||||
|
||||
// inline 以支持浏览器内嵌预览
|
||||
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("inline")
|
||||
{
|
||||
FileName = fileInfo.Name
|
||||
};
|
||||
|
||||
// CORS 允许跨域及暴露必要头
|
||||
if (!response.Headers.Contains("Access-Control-Allow-Origin"))
|
||||
response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
if (!response.Headers.Contains("Access-Control-Expose-Headers"))
|
||||
response.Headers.Add("Access-Control-Expose-Headers", "Accept-Ranges, Content-Range, Content-Length, Content-Type, Content-Disposition");
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.InternalServerError, new { success = false, message = $"预览失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private IHttpActionResult GetFilePreviewLocal(string localPath, string remotePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(localPath);
|
||||
var extension = fileInfo.Extension.ToLowerInvariant();
|
||||
var previewable = IsPreviewableFile(extension);
|
||||
var contentType = GetContentType(fileInfo.Name);
|
||||
|
||||
bool tooLarge = fileInfo.Length > 500 * 1024 * 1024; // 500MB限制
|
||||
string base64String = null;
|
||||
string dataUrl = null;
|
||||
bool canPreview = previewable && !tooLarge;
|
||||
if (canPreview)
|
||||
{
|
||||
var fileBytes = System.IO.File.ReadAllBytes(localPath);
|
||||
base64String = Convert.ToBase64String(fileBytes);
|
||||
dataUrl = $"data:{contentType};base64,{base64String}";
|
||||
}
|
||||
|
||||
var result = new
|
||||
{
|
||||
success = true,
|
||||
type = "file",
|
||||
path = remotePath,
|
||||
name = fileInfo.Name,
|
||||
size = fileInfo.Length,
|
||||
extension = fileInfo.Extension,
|
||||
contentType = contentType,
|
||||
isPreviewable = canPreview,
|
||||
base64Data = base64String,
|
||||
dataUrl = dataUrl,
|
||||
created = fileInfo.CreationTime,
|
||||
modified = fileInfo.LastWriteTime,
|
||||
message = tooLarge ? "文件过大,无法预览,可下载" : null
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new { success = false, message = $"获取文件预览失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private IHttpActionResult GetDirectoryContentsLocal(string localPath, string remotePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = new List<object>();
|
||||
var directories = Directory.GetDirectories(localPath);
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(dir);
|
||||
var subPath = CombineRemotePath(remotePath, dirInfo.Name);
|
||||
items.Add(new
|
||||
{
|
||||
name = dirInfo.Name,
|
||||
type = "directory",
|
||||
path = subPath,
|
||||
created = dirInfo.CreationTime,
|
||||
modified = dirInfo.LastWriteTime,
|
||||
isPreviewable = false
|
||||
});
|
||||
}
|
||||
var files = Directory.GetFiles(localPath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileInfo = new FileInfo(file);
|
||||
var filePath = CombineRemotePath(remotePath, fileInfo.Name);
|
||||
var extension = fileInfo.Extension.ToLowerInvariant();
|
||||
items.Add(new
|
||||
{
|
||||
name = fileInfo.Name,
|
||||
type = "file",
|
||||
path = filePath,
|
||||
size = fileInfo.Length,
|
||||
extension = fileInfo.Extension,
|
||||
created = fileInfo.CreationTime,
|
||||
modified = fileInfo.LastWriteTime,
|
||||
isPreviewable = IsPreviewableFile(extension),
|
||||
contentType = GetContentType(fileInfo.Name)
|
||||
});
|
||||
}
|
||||
var result = new
|
||||
{
|
||||
success = true,
|
||||
type = "directory",
|
||||
path = remotePath,
|
||||
items = items.OrderBy(x => ((dynamic)x).type == "directory" ? 0 : 1)
|
||||
.ThenBy(x => ((dynamic)x).name)
|
||||
.ToList()
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new { success = false, message = $"读取目录失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private IHttpActionResult GetDirectoryContentsRemote(string remotePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var session = OpenSftp())
|
||||
{
|
||||
var dir = session.ListDirectory(remotePath);
|
||||
var items = new List<object>();
|
||||
|
||||
foreach (var subdir in dir.Files.Where(e => e.IsDirectory && e.Name != "." && e.Name != ".."))
|
||||
{
|
||||
var subPath = CombineRemotePath(remotePath, subdir.Name);
|
||||
items.Add(new
|
||||
{
|
||||
name = subdir.Name,
|
||||
type = "directory",
|
||||
path = subPath,
|
||||
created = subdir.LastWriteTime,
|
||||
modified = subdir.LastWriteTime,
|
||||
isPreviewable = false
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var file in dir.Files.Where(e => !e.IsDirectory))
|
||||
{
|
||||
var filePath = CombineRemotePath(remotePath, file.Name);
|
||||
var extension = Path.GetExtension(file.Name).ToLowerInvariant();
|
||||
items.Add(new
|
||||
{
|
||||
name = file.Name,
|
||||
type = "file",
|
||||
path = filePath,
|
||||
size = file.Length,
|
||||
extension = Path.GetExtension(file.Name),
|
||||
created = file.LastWriteTime,
|
||||
modified = file.LastWriteTime,
|
||||
isPreviewable = IsPreviewableFile(extension),
|
||||
contentType = GetContentType(file.Name)
|
||||
});
|
||||
}
|
||||
|
||||
var result = new
|
||||
{
|
||||
success = true,
|
||||
type = "directory",
|
||||
path = remotePath,
|
||||
items = items.OrderBy(x => ((dynamic)x).type == "directory" ? 0 : 1)
|
||||
.ThenBy(x => ((dynamic)x).name)
|
||||
.ToList()
|
||||
};
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new { success = false, message = $"读取远程目录失败: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private bool DownloadFileFromSftp(string remotePath, string localPath, out string error)
|
||||
{
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var localDir = Path.GetDirectoryName(localPath);
|
||||
if (!Directory.Exists(localDir)) Directory.CreateDirectory(localDir);
|
||||
|
||||
using (var session = OpenSftp())
|
||||
{
|
||||
var result = session.GetFiles(remotePath, localPath, false);
|
||||
result.Check();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Session OpenSftp()
|
||||
{
|
||||
var options = new SessionOptions
|
||||
{
|
||||
Protocol = Protocol.Sftp,
|
||||
HostName = _sftpHost,
|
||||
UserName = _sftpUser,
|
||||
Password = _sftpPassword,
|
||||
SshHostKeyPolicy = SshHostKeyPolicy.GiveUpSecurityAndAcceptAny
|
||||
};
|
||||
var session = new Session();
|
||||
session.Open(options);
|
||||
return session;
|
||||
}
|
||||
|
||||
private void EnsureCacheRoot()
|
||||
{
|
||||
if (!Directory.Exists(_cacheRoot)) Directory.CreateDirectory(_cacheRoot);
|
||||
}
|
||||
|
||||
private string NormalizeRemotePath(string p)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p)) return "/";
|
||||
var s = p.Trim();
|
||||
// 统一使用正斜杠
|
||||
s = s.Replace('\\', '/');
|
||||
return s;
|
||||
}
|
||||
|
||||
private string ToLocalPath(string remotePath)
|
||||
{
|
||||
// 去掉开头的 '/'
|
||||
var relative = remotePath.Trim().TrimStart('/');
|
||||
var localRelative = relative.Replace('/', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(_cacheRoot, localRelative);
|
||||
}
|
||||
|
||||
private string CombineRemotePath(string basePath, string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(basePath) || basePath == "/") return $"/{name}";
|
||||
if (basePath.EndsWith("/")) return basePath + name;
|
||||
return basePath + "/" + name;
|
||||
}
|
||||
|
||||
private bool IsPreviewableFile(string extension)
|
||||
{
|
||||
var previewableExtensions = new[]
|
||||
{
|
||||
".bmp", ".jpg", ".jpeg", ".png", ".gif", ".tiff", ".tif", ".webp",
|
||||
".txt", ".json", ".xml", ".csv", ".log", ".md", ".pdf"
|
||||
};
|
||||
return previewableExtensions.Contains(extension);
|
||||
}
|
||||
|
||||
private string GetContentType(string fileName)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
if (extension == ".bmp") return "image/bmp";
|
||||
if (extension == ".jpg" || extension == ".jpeg") return "image/jpeg";
|
||||
if (extension == ".png") return "image/png";
|
||||
if (extension == ".gif") return "image/gif";
|
||||
if (extension == ".tiff" || extension == ".tif") return "image/tiff";
|
||||
if (extension == ".webp") return "image/webp";
|
||||
if (extension == ".pdf") return "application/pdf";
|
||||
if (extension == ".txt") return "text/plain";
|
||||
if (extension == ".json") return "application/json";
|
||||
if (extension == ".xml") return "application/xml";
|
||||
if (extension == ".csv") return "text/csv";
|
||||
if (extension == ".md") return "text/markdown";
|
||||
if (extension == ".log") return "text/plain";
|
||||
if (extension == ".zip") return "application/zip";
|
||||
if (extension == ".rar") return "application/x-rar-compressed";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user