修复压缩失败恢复并校验归档完整性
This commit is contained in:
@@ -12,16 +12,16 @@ public class GalleryTask {
|
|||||||
public static final byte COMPRESS_COMPLETE = 4;
|
public static final byte COMPRESS_COMPLETE = 4;
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
private String name;
|
private volatile String name;
|
||||||
|
|
||||||
private int gid;
|
private int gid;
|
||||||
|
|
||||||
private byte status;
|
private volatile byte status;
|
||||||
|
|
||||||
private int proceeding;
|
private volatile int proceeding;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String path;
|
private volatile String path;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean is_download_complete(){
|
public boolean is_download_complete(){
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import java.io.*;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DownloadCheckService {
|
public class DownloadCheckService {
|
||||||
@@ -23,11 +28,20 @@ public class DownloadCheckService {
|
|||||||
|
|
||||||
final ArrayList<GalleryTask> compress_queue;
|
final ArrayList<GalleryTask> compress_queue;
|
||||||
|
|
||||||
|
final Map<Integer, Long> retryAfter = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public DownloadCheckService(Map<Integer, GalleryTask> queue){
|
public DownloadCheckService(Map<Integer, GalleryTask> queue){
|
||||||
|
this(queue, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests use temporary directories and invoke scans explicitly.
|
||||||
|
DownloadCheckService(Map<Integer, GalleryTask> queue, boolean startScheduler){
|
||||||
this.queue = queue;
|
this.queue = queue;
|
||||||
compress_queue = new ArrayList<>(0);
|
compress_queue = new ArrayList<>(0);
|
||||||
|
if (startScheduler) {
|
||||||
convert_thread = new ScheduledThreadPoolExecutor(1);
|
convert_thread = new ScheduledThreadPoolExecutor(1);
|
||||||
convert_thread.scheduleAtFixedRate(this::compress, 0, 5, TimeUnit.SECONDS);
|
convert_thread.scheduleWithFixedDelay(this::compress, 0, 5, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean downloadCheck(){
|
public boolean downloadCheck(){
|
||||||
@@ -55,7 +69,7 @@ public class DownloadCheckService {
|
|||||||
|
|
||||||
while(fileIterator.hasNext()){
|
while(fileIterator.hasNext()){
|
||||||
File file = fileIterator.next();
|
File file = fileIterator.next();
|
||||||
if(!file.getName().contains(String.valueOf(galleryTask.getGid())))
|
if(!file.isDirectory() || !matchesGid(file.getName(), galleryTask.getGid()))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
galleryTask.setStatus(GalleryTask.DOWNLOADING);
|
galleryTask.setStatus(GalleryTask.DOWNLOADING);
|
||||||
@@ -81,7 +95,8 @@ public class DownloadCheckService {
|
|||||||
|
|
||||||
//压缩队列
|
//压缩队列
|
||||||
for(GalleryTask galleryTask: queue.values())
|
for(GalleryTask galleryTask: queue.values())
|
||||||
if (galleryTask.is_download_complete()) {
|
if (galleryTask.is_download_complete()
|
||||||
|
&& System.currentTimeMillis() >= retryAfter.getOrDefault(galleryTask.getGid(), 0L)) {
|
||||||
galleryTask.setStatus(GalleryTask.COMPRESSING);
|
galleryTask.setStatus(GalleryTask.COMPRESSING);
|
||||||
synchronized (compress_queue) {
|
synchronized (compress_queue) {
|
||||||
compress_queue.add(galleryTask);
|
compress_queue.add(galleryTask);
|
||||||
@@ -95,36 +110,87 @@ public class DownloadCheckService {
|
|||||||
* 压缩线程:将压缩队列的任务复制一份,进行转换
|
* 压缩线程:将压缩队列的任务复制一份,进行转换
|
||||||
*/
|
*/
|
||||||
public void compress() {
|
public void compress() {
|
||||||
if(compress_queue.isEmpty())
|
|
||||||
return;
|
|
||||||
ArrayList<GalleryTask> galleryTasks;
|
ArrayList<GalleryTask> galleryTasks;
|
||||||
synchronized (compress_queue) {
|
synchronized (compress_queue) {
|
||||||
|
if (compress_queue.isEmpty())
|
||||||
|
return;
|
||||||
galleryTasks = new ArrayList<>(compress_queue);
|
galleryTasks = new ArrayList<>(compress_queue);
|
||||||
compress_queue.clear();
|
compress_queue.clear();
|
||||||
}
|
}
|
||||||
for (GalleryTask galleryTask : galleryTasks) {
|
for (GalleryTask galleryTask : galleryTasks) {
|
||||||
|
Path temporary = null;
|
||||||
try {
|
try {
|
||||||
log.info("开始压缩:{}", galleryTask.getName());
|
log.info("开始压缩:{}", galleryTask.getName());
|
||||||
File file = new File(storagePath + galleryTask.getName());
|
Path directory = Paths.get(storagePath, galleryTask.getName());
|
||||||
if (file.isDirectory() || file.mkdirs()) {
|
Files.createDirectories(directory);
|
||||||
log.info("{}文件夹创建成功", galleryTask.getName());
|
Path archive = directory.resolve(galleryTask.getName() + ".zip");
|
||||||
} else {
|
temporary = Files.createTempFile(directory, ".compress-", ".zip.part");
|
||||||
log.error("{}文件夹创建失败", galleryTask.getName());
|
ZipUtil.zip(galleryTask.getPath(), temporary.toString());
|
||||||
continue;
|
if (!isValidArchive(temporary.toFile()))
|
||||||
|
throw new IOException("压缩包校验失败");
|
||||||
|
// Publish only a closed, verified archive. A crash leaves a .part file.
|
||||||
|
try {
|
||||||
|
Files.move(temporary, archive, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
Files.move(temporary, archive, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
temporary = null;
|
||||||
ZipUtil.zip(galleryTask.getPath(), storagePath + galleryTask.getName() + "/" + galleryTask.getName() + ".zip");
|
|
||||||
log.info("{}压缩完成", galleryTask.getName());
|
|
||||||
|
|
||||||
FileUtil.del(galleryTask.getPath());
|
|
||||||
galleryTask.setStatus(GalleryTask.COMPRESS_COMPLETE);
|
galleryTask.setStatus(GalleryTask.COMPRESS_COMPLETE);
|
||||||
|
retryAfter.remove(galleryTask.getGid());
|
||||||
|
if (!FileUtil.del(galleryTask.getPath()))
|
||||||
|
log.warn("压缩已完成,但源目录清理失败: {}", galleryTask.getPath());
|
||||||
|
log.info("{}压缩完成", galleryTask.getName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("{}压缩失败:{}", galleryTask, e.getMessage());
|
// Keep the source and restore an existing, retryable protocol state.
|
||||||
|
if (!galleryTask.is_compress_complete()) {
|
||||||
|
retryAfter.put(galleryTask.getGid(), System.currentTimeMillis() + 30_000);
|
||||||
|
galleryTask.setStatus(GalleryTask.DOWNLOAD_COMPLETE);
|
||||||
}
|
}
|
||||||
|
log.error("{}压缩或清理失败,源文件保留,稍后可重试", galleryTask.getName(), e);
|
||||||
|
} finally {
|
||||||
|
if (temporary != null) {
|
||||||
|
try { Files.deleteIfExists(temporary); }
|
||||||
|
catch (IOException e) { log.warn("清理压缩临时文件失败: {}", temporary, e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean isValidArchive(File file) {
|
||||||
|
if (!file.isFile())
|
||||||
|
return false;
|
||||||
|
try (ZipFile zip = new ZipFile(file)) {
|
||||||
|
if (zip.size() == 0)
|
||||||
|
return false;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||||
|
while (entries.hasMoreElements()) {
|
||||||
|
ZipEntry entry = entries.nextElement();
|
||||||
|
if (entry.isDirectory())
|
||||||
|
continue;
|
||||||
|
CRC32 crc = new CRC32();
|
||||||
|
long size = 0;
|
||||||
|
try (InputStream input = zip.getInputStream(entry)) {
|
||||||
|
int count;
|
||||||
|
while ((count = input.read(buffer)) != -1) {
|
||||||
|
crc.update(buffer, 0, count);
|
||||||
|
size += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (size != entry.getSize() || crc.getValue() != entry.getCrc())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public GalleryTask addToQueue(GalleryTask galleryTask){
|
public GalleryTask addToQueue(GalleryTask galleryTask){
|
||||||
|
GalleryTask active = queue.get(galleryTask.getGid());
|
||||||
|
if (active != null && active.is_compressing())
|
||||||
|
return active;
|
||||||
|
retryAfter.remove(galleryTask.getGid());
|
||||||
// A reconnect can resend a task whose name is stale (for example, the
|
// A reconnect can resend a task whose name is stale (for example, the
|
||||||
// downloader appended a resolution suffix). Resolve completed archives
|
// downloader appended a resolution suffix). Resolve completed archives
|
||||||
// by gid first, because the gid is stable while the directory name is not.
|
// by gid first, because the gid is stable while the directory name is not.
|
||||||
@@ -176,7 +242,7 @@ public class DownloadCheckService {
|
|||||||
|
|
||||||
for(File directory : directories){
|
for(File directory : directories){
|
||||||
if(matchesGid(directory.getName(), gid)
|
if(matchesGid(directory.getName(), gid)
|
||||||
&& new File(directory, directory.getName() + ".zip").isFile())
|
&& isValidArchive(new File(directory, directory.getName() + ".zip")))
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class storageNode {
|
|||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
return thread;
|
return thread;
|
||||||
});
|
});
|
||||||
queue = new HashMap<>();
|
queue = new ConcurrentHashMap<>();
|
||||||
tempQueue = new HashMap<>();
|
tempQueue = new HashMap<>();
|
||||||
lock = new ReentrantLock();
|
lock = new ReentrantLock();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package lion.Service;
|
||||||
|
|
||||||
|
import lion.Domain.GalleryTask;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class DownloadCheckServiceTest {
|
||||||
|
@Test
|
||||||
|
void failedCompressionRetainsSourceAndCanRetry(@TempDir Path root) throws Exception {
|
||||||
|
var queue = new ConcurrentHashMap<Integer, GalleryTask>();
|
||||||
|
var service = new DownloadCheckService(queue, false);
|
||||||
|
Path downloads = Files.createDirectory(root.resolve("downloads"));
|
||||||
|
Path source = Files.createDirectory(downloads.resolve("sample [123]"));
|
||||||
|
Files.writeString(source.resolve("galleryinfo.txt"), "metadata");
|
||||||
|
Files.writeString(source.resolve("1.jpg"), "image bytes");
|
||||||
|
Path storage = root.resolve("storage");
|
||||||
|
Files.writeString(storage, "block mkdir");
|
||||||
|
service.downloadPath = downloads.toString();
|
||||||
|
service.storagePath = storage.toString();
|
||||||
|
GalleryTask task = new GalleryTask();
|
||||||
|
task.setGid(123);
|
||||||
|
service.addToQueue(task);
|
||||||
|
service.downloadCheck();
|
||||||
|
service.compress();
|
||||||
|
assertEquals(GalleryTask.DOWNLOAD_COMPLETE, task.getStatus());
|
||||||
|
assertTrue(Files.exists(source.resolve("1.jpg")));
|
||||||
|
Files.delete(storage);
|
||||||
|
Files.createDirectory(storage);
|
||||||
|
service.addToQueue(task); // manual retry removes the backoff
|
||||||
|
service.downloadCheck();
|
||||||
|
service.compress();
|
||||||
|
assertEquals(GalleryTask.COMPRESS_COMPLETE, task.getStatus());
|
||||||
|
assertTrue(DownloadCheckService.isValidArchive(storage.resolve("sample [123]/sample [123].zip").toFile()));
|
||||||
|
assertFalse(Files.exists(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void corruptArchiveIsNotTreatedAsCompleted(@TempDir Path root) throws Exception {
|
||||||
|
Path stored = Files.createDirectories(root.resolve("stored/sample [123]"));
|
||||||
|
Files.writeString(stored.resolve("sample [123].zip"), "partial zip");
|
||||||
|
var service = new DownloadCheckService(new ConcurrentHashMap<>(), false);
|
||||||
|
service.storagePath = root.resolve("stored").toString();
|
||||||
|
service.downloadPath = root.resolve("downloads").toString();
|
||||||
|
GalleryTask task = new GalleryTask();
|
||||||
|
task.setGid(123);
|
||||||
|
assertFalse(service.addToQueue(task).is_compress_complete());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user