c#实现定时从外部服务器获取文件并查重(MD5)
需求:需要定时去请求外部服务器的文件,看看每天是否有新的文件上传,如果有就下载到本地服务器,并记录数据。原来的文件重命名。
方案:这里通过文件的MD5和其他条件来判断文件是否存在。因为文件量过大,所以批量下载的时候有时候会出现部分文件没能下载成功,但是数据入库了,所以这里也做了判断,没能下载成功的文件会再次下载但是数据不会入库
注意:MD5与文件名称没有关系,和文件内容有关,文件内容不同则生成的MD5不同
1 1 public class FileTimerHelper 2 2 { 3 3 4 4 private static string wwwRootPath = FilePath.RootPath; 5 5 private static Timer _dailyTaskTimer; 6 6 7 7 8 8 public static void TimerInit() 9 9 { 10 10 Console.WriteLine("正在初始化每日任务定时器..."); 11 11 // 初始化定时器 12 12 _dailyTaskTimer = new Timer(); 13 13 _dailyTaskTimer.Elapsed += new ElapsedEventHandler(OnDailyTaskTimer); 14 14 _dailyTaskTimer.Interval = GeUntilNext8AM(); 15 15 _dailyTaskTimer.AutoReset = true; // 每天执行 16 16 _dailyTaskTimer.Enabled = true; 17 17 Console.WriteLine("每日任务定时器已启动,将于每天8点执行。"); 18 18 //_dailyTaskTimer = new Timer(); 19 19 //_dailyTaskTimer.Elapsed += new ElapsedEventHandler(OnDailyTaskTimerElapsed); 20 20 //_dailyTaskTimer.Interval = TimeSpan.FromMinutes(3).TotalMilliseconds; // 3分钟 21 21 //_dailyTaskTimer.AutoReset = false; // 只执行一次 22 22 //_dailyTaskTimer.Enabled = true; 23 23 //Console.WriteLine("每日任务定时器已启动,将于3分钟后执行。"); 24 24 25 25 26 26 } 27 27 public static void ExecuteDailyTask() 28 28 { 29 29 try 30 30 { 31 31 string phpFileUrl = $"{ServerConfig.DocUrl}/xxx.php"; 32 32 string folderPath = Path.Combine(wwwRootPath, "File", "保存文件的文件夹名称"); 33 33 Directory.CreateDirectory(folderPath); 34 34 var requestData = new Dictionary<string, dynamic> { { "请求参数", "XXX" } }; 35 35 var responseContent = HttpServer.Post(phpFileUrl, requestData); 36 36 var json = JsonConvert.DeserializeObject<ErpDoc>(responseContent); 37 37 38 38 var details = new List<ProductionDocument>();//待入库文件数据,productionDocument是实体类 39 39 var fileUrls = new List<FileUrls>();//待下载文件列表 40 40 foreach (var row in json.Rows) 41 41 { 42 42 string fileUrl = $"{ServerConfig.DocUrl}/" + row.File; 43 43 44 44 using (var client = new System.Net.Http.HttpClient()) 45 45 { 46 46 client.Timeout = TimeSpan.FromSeconds(60); 47 47 using (var response = client.GetAsync(fileUrl).Result) // 更改为Get并使用.Result等待完成 48 48 { 49 49 response.EnsureSuccessStatusCode(); 50 50 byte[] fileBytes = response.Content.ReadAsByteArrayAsync().Result; // 使用.Result等待完成 51 51 string fileBase64 = Convert.ToBase64String(fileBytes); 52 52 string fileMD5 = fileBase64.GetMd5Hash(); 53 53 54 54 string relativePath = Path.Combine(folderPath, string.Join("/", row.File.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).Skip(1).Take(row.File.Split('/').Length - 2))); 55 55 if (!FileExistsInDatabaseAsync(row, relativePath, fileMD5)) 56 56 { 57 57 58 58 Directory.CreateDirectory(relativePath); 59 59 string fileName = Path.GetFileName(row.File); 60 60 string destinationFile = Path.Combine(relativePath, fileName); 61 61 string databasePath = destinationFile.Substring(wwwRootPath.Length).TrimStart(Path.DirectorySeparatorChar).Replace(Path.DirectorySeparatorChar, '/'); 62 62 63 63 details.Add(new ProductionDocument 64 64 { 65 65 CreateUserId = "admin", 66 66 MarkedDateTime = DateTime.UtcNow, 67 67 WorkshopId = 1, 68 68 Level = row.Jb, 69 69 Unit = row.Bm, 70 70 Number = row.Bh, 71 71 Name = row.Fullname, 72 72 File = row.File, 73 73 FileNew = databasePath, 74 74 AddTime = DateTime.UtcNow, 75 75 MD5 = fileMD5 76 76 }); 77 77 78 78 fileUrls.Add(new FileUrls { GetFileUrl = fileUrl, TarGetFileUrl = destinationFile }); 79 79 } 80 80 } 81 81 } 82 82 } 83 83 84 84 // 使用 ParallelOptions 来限制并发数量 85 85 var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; 86 86 Parallel.ForEach(fileUrls, parallelOptions, fileUrl => 87 87 { 88 88 try 89 89 { 90 90 DownloadFileAsync(fileUrl); 91 91 92 92 } 93 93 catch (Exception ex) 94 94 { 95 95 // 记录下载失败的错误信息 96 96 Console.WriteLine($"文件下载失败: {fileUrl.GetFileUrl}: {ex.Message}"); 97 97 } 98 98 }); 99 99 100 100 101 101 ProductionDocumentHelper.Instance.Add<ProductionDocument>(details); 102 102 103 103 } 104 104 catch (Exception ex) 105 105 { 106 106 Console.WriteLine(ex.Message); 107 107 } 108 108 } 109 109 110 110 public static async void DownloadFileAsync(FileUrls fileUrls) 111 111 { 112 112 // 下载文件并保存到指定路径 113 113 using (var client = new System.Net.Http.HttpClient()) 114 114 { 115 115 client.Timeout = TimeSpan.FromSeconds(60); 116 116 using (HttpResponseMessage response = await client.GetAsync(fileUrls.GetFileUrl)) 117 117 { 118 118 response.EnsureSuccessStatusCode(); 119 119 using (FileStream fileStream = new FileStream(fileUrls.TarGetFileUrl, FileMode.Create, FileAccess.Write, FileShare.None)) 120 120 { 121 121 await response.Content.CopyToAsync(fileStream); 122 122 } 123 123 124 124 } 125 125 } 126 126 127 127 128 128 } 129 129 130 130 private static bool FileExistsInDatabaseAsync(RowData row, string relativePath, string MD5) 131 131 { 132 132 133 133 string fileUrl = $"{ServerConfig.DocUrl}/" + row.File; 134 134 // 检查数据库中是否已存在该文件记录 135 135 Directory.CreateDirectory(relativePath); 136 136 var docList = ProductionDocumentHelper.Instance.Gets(row.Jb, row.Bm);//含删除 137 137 var ifOther = docList.Where(e => e.Number == row.Bh && e.Name.Contains(row.Fullname)); 138 138 string destinationFile = Path.Combine(relativePath, Path.GetFileName(row.File)); 139 139 140 140 if (ifOther.Count() > 0) 141 141 { 142 142 var ifAnyMd5 = ifOther.Where(e => e.MD5 == MD5); 143 143 if (ifAnyMd5.Count() > 0) 144 144 { //如果该文件原来有旧数据已经删除,则不重新下载 145 145 var delAny = ifAnyMd5.Where(e => e.MarkedDelete == true); 146 146 if (delAny.Any()) 147 147 { 148 148 return true; 149 149 } 150 150 else 151 151 { 152 152 if (!System.IO.File.Exists(destinationFile)) 153 153 { 154 154 155 155 FileUrls fileUrls = new FileUrls { GetFileUrl = fileUrl, TarGetFileUrl = destinationFile }; 156 156 //批量下载文件有时候会出现文件未成功下载的问题,如果检测到有数据未下载,重新下载文件 157 157 DownloadFileAsync(fileUrls); 158 158 159 159 } 160 160 } 161 161 return true; 162 162 } 163 163 164 164 else 165 165 { //如果部门-级别-编号-文件名都一致但是文件内容(MD5)不一致,删除原来的数据,保留最新的 166 166 var delFirst = ifOther.Where(e => e.MarkedDelete == false).FirstOrDefault(); 167 167 ProductionDocumentHelper.Instance.Delete(delFirst.Id); 168 168 169 169 // 重命名文件,如果存在同名文件 170 170 string extension = Path.GetExtension(Path.GetFileName(row.File)); 171 171 string baseName = Path.GetFileNameWithoutExtension(Path.GetFileName(row.File)); 172 172 int i = 1; 173 173 while (File.Exists(destinationFile)) 174 174 { 175 175 // 生成新的文件名 176 176 string newFileName = $"{baseName}({i}){extension}"; 177 177 string newFileFullPath = Path.Combine(relativePath, newFileName); 178 178 if (!File.Exists(newFileFullPath)) 179 179 { 180 180 // 重命名文件 181 181 File.Move(destinationFile, newFileFullPath); 182 182 // 移动文件 183 183 184 184 // 更新文件路径为新的文件名 185 185 destinationFile = newFileFullPath; 186 186 break; 187 187 } 188 188 i++; 189 189 } 190 190 191 191 return false; 192 192 } 193 193 } 194 194 else 195 195 { 196 196 197 197 return false; 198 198 } 199 199 } 200 200 201 201 202 202 203 203 private static void OnDailyTaskTimer(object source, ElapsedEventArgs e) 204 204 { 205 205 try 206 206 { 207 207 Console.WriteLine("定时任务开始执行..."); 208 208 FileTimerHelper.ExecuteDailyTask(); 209 209 Console.WriteLine("定时任务执行完毕。"); 210 210 } 211 211 catch (Exception ex) 212 212 { 213 213 // 处理异常 214 214 Console.WriteLine($"定时任务执行过程中发生错误: {ex.Message}"); 215 215 } 216 216 } 217 217 // 计算到下一个8点的间隔时间 218 218 private static double GeUntilNext8AM() 219 219 { 220 220 var now = DateTime.Now; 221 221 var tomorrow = now.Date.AddDays(1); 222 222 var next8AM = new DateTime(tomorrow.Year, tomorrow.Month, tomorrow.Day, 8, 0, 0); 223 223 224 224 return (next8AM - now).TotalMilliseconds; 225 225 } 226 226 227 227 // 在应用程序关闭时,确保释放定时器资源 228 228 public static void OnApplicationShutdown() 229 229 { 230 230 _dailyTaskTimer?.Dispose(); 231 231 } 232 232 233 233 234 234 235 235 }
1 public class ServerConfig 2 { 3 public static DataBase ApiDb; 4 public static string DocUrl; 5 6 public static void Init(IConfiguration configuration) 7 { 8 ApiDb = new DataBase(configuration.GetConnectionString("ApiDb")); 9 10 DocUrl = configuration.GetAppSettings<string>("DocUrl"); 11 FileTimerHelper.TimerInit(); 12 } 13 14 }
1 (HTTpServer 和HttpClient都是自定义的,仅供参考 ) 2 public static string Post(string url, Dictionary<string, dynamic> data) 3 { 4 try 5 { 6 var httpClient = new HttpClient(url) { Verb = HttpVerb.POST }; 7 foreach (var dt in data) 8 { 9 httpClient.PostingData.Add(dt.Key, dt.Value.ToString()); 10 } 11 12 var result = httpClient.GetString(); 13 14 return result; 15 } 16 catch (Exception e) 17 { 18 if (url.Contains("XXX")) 19 { 20 Log.Error($"请求服务器异常 Post Form:{url},{data.ToJSON()},{e}"); 21 } 22 else 23 { 24 Log.Error($"请求服务器异常 Post Form:{url},{e}"); 25 } 26 return "fail"; 27 } 28 }
Startup.cs中