package nu.marginalia.svc; import com.google.inject.Inject; import nu.marginalia.storage.FileStorageService; import nu.marginalia.storage.model.FileStorageId; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import spark.Request; import spark.Response; import spark.Spark; import javax.servlet.ServletOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.sql.SQLException; public class ExecutorFileTransferService { private final FileStorageService fileStorageService; private static final Logger logger = LoggerFactory.getLogger(ExecutorFileTransferService.class); @Inject public ExecutorFileTransferService(FileStorageService fileStorageService) { this.fileStorageService = fileStorageService; } /** Allows transfer of files from each partition */ public Object transferFile(Request request, Response response) throws SQLException, IOException { FileStorageId fileStorageId = FileStorageId.parse(request.params("fid")); var fileStorage = fileStorageService.getStorage(fileStorageId); Path basePath = fileStorage.asPath(); String path = request.queryParams("path").replaceAll("%2F", "/"); Path filePath = basePath.resolve(path).normalize(); // ensure filePath is within basePath // even though this is an internal API, it's better to be safe than sorry if (!filePath.startsWith(basePath)) { response.status(403); return "Forbidden"; } // Announce that we support byte ranges response.header("Accept-Ranges", "bytes"); // Set the content type to binary response.type("application/octet-stream"); String range = request.headers("Range"); if (range != null) { String[] ranges = StringUtils.split(range, '='); if (ranges.length != 2 || !"bytes".equals(ranges[0])) { logger.warn("Invalid range header in {}: {}", filePath, range); Spark.halt(400, "Invalid range header"); } String[] rangeValues = StringUtils.split(ranges[1], '-'); if (rangeValues.length != 2) { logger.warn("Invalid range header in {}: {}", filePath, range); Spark.halt(400, "Invalid range header"); } long start = Long.parseLong(rangeValues[0].trim()); long end = Long.parseLong(rangeValues[1].trim()); long contentLength = end - start + 1; response.header("Content-Range", "bytes " + start + "-" + end + "/" + Files.size(filePath)); response.header("Content-Length", String.valueOf(contentLength)); response.status(206); if ("HEAD".equalsIgnoreCase(request.requestMethod())) { return ""; } serveFile(filePath, response.raw().getOutputStream(), start, end + 1); } else { response.header("Content-Length", String.valueOf(Files.size(filePath))); response.status(200); if ("HEAD".equalsIgnoreCase(request.requestMethod())) { return ""; } serveFile(filePath, response.raw().getOutputStream()); } return ""; } private void serveFile(Path filePath, ServletOutputStream outputStream) throws IOException { try (var is = Files.newInputStream(filePath)) { IOUtils.copy(is, outputStream); } } private void serveFile(Path filePath, ServletOutputStream outputStream, long start, long end) throws IOException { try (var is = Files.newInputStream(filePath)) { IOUtils.copyLarge(is, outputStream, start, end - start); } } }