(converter) File listing and download for file storage

This commit is contained in:
Viktor Lofgren 2023-07-26 21:59:35 +02:00
parent a5d980ee56
commit 66bb12e55a
5 changed files with 115 additions and 7 deletions

View File

@ -85,6 +85,7 @@ public class ControlService extends Service {
Spark.get("/public/storage/crawls", this::storageModelCrawls, storageCrawlsRenderer::render); Spark.get("/public/storage/crawls", this::storageModelCrawls, storageCrawlsRenderer::render);
Spark.get("/public/storage/processed", this::storageModelProcessed, storageProcessedRenderer::render); Spark.get("/public/storage/processed", this::storageModelProcessed, storageProcessedRenderer::render);
Spark.get("/public/storage/:id", this::storageDetailsModel, storageDetailsRenderer::render); Spark.get("/public/storage/:id", this::storageDetailsModel, storageDetailsRenderer::render);
Spark.get("/public/storage/:id/file", controlFileStorageService::downloadFileFromStorage);
final HtmlRedirect redirectToServices = new HtmlRedirect("/services"); final HtmlRedirect redirectToServices = new HtmlRedirect("/services");

View File

@ -0,0 +1,15 @@
package nu.marginalia.control.model;
import nu.marginalia.db.storage.model.FileStorage;
import java.util.List;
public record FileStorageFileModel(String filename,
String type,
String size
) {
public boolean isDownloadable() {
return type.equals("file");
}
}

View File

@ -5,6 +5,9 @@ import nu.marginalia.db.storage.model.FileStorageType;
import java.util.List; import java.util.List;
public record FileStorageWithRelatedEntries(FileStorageWithActions self, List<FileStorage> related) { public record FileStorageWithRelatedEntries(FileStorageWithActions self,
List<FileStorage> related,
List<FileStorageFileModel> files
) {
} }

View File

@ -7,19 +7,23 @@ import lombok.SneakyThrows;
import nu.marginalia.control.model.*; import nu.marginalia.control.model.*;
import nu.marginalia.db.storage.FileStorageService; import nu.marginalia.db.storage.FileStorageService;
import nu.marginalia.db.storage.model.*; import nu.marginalia.db.storage.model.*;
import nu.marginalia.mqsm.graph.AbstractStateGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Singleton @Singleton
public class ControlFileStorageService { public class ControlFileStorageService {
private final HikariDataSource dataSource; private final HikariDataSource dataSource;
private final FileStorageService fileStorageService; private final FileStorageService fileStorageService;
private Logger logger = LoggerFactory.getLogger(getClass());
@Inject @Inject
public ControlFileStorageService(HikariDataSource dataSource, FileStorageService fileStorageService) { public ControlFileStorageService(HikariDataSource dataSource, FileStorageService fileStorageService) {
@ -107,9 +111,47 @@ public class ControlFileStorageService {
public FileStorageWithRelatedEntries getFileStorageWithRelatedEntries(FileStorageId id) throws SQLException { public FileStorageWithRelatedEntries getFileStorageWithRelatedEntries(FileStorageId id) throws SQLException {
var storage = fileStorageService.getStorage(id); var storage = fileStorageService.getStorage(id);
var related = getRelatedEntries(id); var related = getRelatedEntries(id);
return new FileStorageWithRelatedEntries(new FileStorageWithActions(storage), related);
List<FileStorageFileModel> files = new ArrayList<>();
try (var filesStream = Files.list(storage.asPath())) {
filesStream
.map(this::createFileModel)
.sorted(Comparator
.comparing(FileStorageFileModel::type)
.thenComparing(FileStorageFileModel::filename)
)
.forEach(files::add);
}
catch (IOException ex) {
logger.error("Failed to list files in storage", ex);
} }
return new FileStorageWithRelatedEntries(new FileStorageWithActions(storage), related, files);
}
private FileStorageFileModel createFileModel(Path p) {
try {
String type = Files.isRegularFile(p) ? "file" : "directory";
String size;
if (Files.isDirectory(p)) {
size = "-";
}
else {
long sizeBytes = Files.size(p);
if (sizeBytes < 1024) size = sizeBytes + " B";
else if (sizeBytes < 1024 * 1024) size = sizeBytes / 1024 + " KB";
else if (sizeBytes < 1024 * 1024 * 1024) size = sizeBytes / (1024 * 1024) + " MB";
else size = sizeBytes / (1024 * 1024 * 1024) + " GB";
}
return new FileStorageFileModel(p.toFile().getName(), type, size);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private List<FileStorage> getRelatedEntries(FileStorageId id) { private List<FileStorage> getRelatedEntries(FileStorageId id) {
List<FileStorage> ret = new ArrayList<>(); List<FileStorage> ret = new ArrayList<>();
try (var conn = dataSource.getConnection(); try (var conn = dataSource.getConnection();
@ -131,4 +173,30 @@ public class ControlFileStorageService {
} }
return ret; return ret;
} }
public Object downloadFileFromStorage(Request request, Response response) throws SQLException {
var fileStorageId = FileStorageId.parse(request.params("id"));
String filename = request.queryParams("name");
Path root = fileStorageService.getStorage(fileStorageId).asPath();
Path filePath = root.resolve(filename).normalize();
if (!filePath.startsWith(root)) {
response.status(403);
return "";
}
if (filePath.endsWith(".txt") || filePath.endsWith(".log")) response.type("text/plain");
else response.type("application/octet-stream");
try (var is = Files.newInputStream(filePath)) {
is.transferTo(response.raw().getOutputStream());
}
catch (IOException ex) {
logger.error("Failed to download file", ex);
throw new RuntimeException(ex);
}
return "";
}
} }

View File

@ -24,6 +24,27 @@
</tr> </tr>
</table> </table>
{{/with}} {{/with}}
{{#if storage.files}}
<h1>Contents </h1>
<table>
<tr>
<th>File Name</th>
<th>Type</th>
<th>Size</th>
</tr>
{{#each storage.files}}
<tr>
<td>
{{#if downloadable}}<a href="/storage/{{storage.self.storage.id}}/file?name={{filename}}">{{filename}}</a></td>
{{else}} {{filename}} {{/if}}
<td>{{type}}</td>
<td>{{size}}</td>
</tr>
{{/each}}
</table>
{{/if}}
<h2>Actions</h2> <h2>Actions</h2>
{{#with storage.self}} {{#with storage.self}}
{{#if isCrawlable}} {{#if isCrawlable}}