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(); 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(); 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"; } } }