190 lines
5.8 KiB
Java
190 lines
5.8 KiB
Java
package nl.andrewlalis.gymboardcdn.service;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import nl.andrewlalis.gymboardcdn.api.FileMetadataResponse;
|
|
import nl.andrewlalis.gymboardcdn.model.FileMetadata;
|
|
import nl.andrewlalis.gymboardcdn.util.ULID;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.web.server.ResponseStatusException;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.time.Instant;
|
|
import java.time.LocalDateTime;
|
|
import java.time.ZoneOffset;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
|
/**
|
|
* The service that manages storing and retrieving files from a base filesystem.
|
|
*/
|
|
@Service
|
|
public class FileService {
|
|
private static final Logger log = LoggerFactory.getLogger(FileService.class);
|
|
|
|
@Value("${app.files.storage-dir}")
|
|
private String storageDir;
|
|
|
|
@Value("${app.files.temp-dir}")
|
|
private String tempDir;
|
|
|
|
private final ULID ulid;
|
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
public FileService(ULID ulid) {
|
|
this.ulid = ulid;
|
|
}
|
|
|
|
public Path getStoragePathForFile(String rawId) {
|
|
ULID.Value id = ULID.parseULID(rawId);
|
|
LocalDateTime time = dateFromULID(id);
|
|
Path dir = Path.of(storageDir)
|
|
.resolve(Integer.toString(time.getYear()))
|
|
.resolve(Integer.toString(time.getMonthValue()))
|
|
.resolve(Integer.toString(time.getDayOfMonth()));
|
|
return dir.resolve(rawId);
|
|
}
|
|
|
|
public String createNewFileIdentifier() {
|
|
return ulid.nextULID();
|
|
}
|
|
|
|
public Path saveToTempFile(InputStream in, String filename) throws IOException {
|
|
Path tempDir = getTempDir();
|
|
String suffix = null;
|
|
if (filename != null) {
|
|
int idx = filename.lastIndexOf('.');
|
|
if (idx >= 0) {
|
|
suffix = filename.substring(idx);
|
|
}
|
|
}
|
|
Path tempFile = Files.createTempFile(tempDir, null, suffix);
|
|
try (var out = Files.newOutputStream(tempFile)) {
|
|
in.transferTo(out);
|
|
}
|
|
return tempFile;
|
|
}
|
|
|
|
private static LocalDateTime dateFromULID(ULID.Value value) {
|
|
return Instant.ofEpochMilli(value.timestamp())
|
|
.atOffset(ZoneOffset.UTC)
|
|
.toLocalDateTime();
|
|
}
|
|
|
|
private Path getStorageDir() throws IOException {
|
|
Path dir = Path.of(storageDir);
|
|
if (Files.notExists(dir)) {
|
|
Files.createDirectories(dir);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
private Path getTempDir() throws IOException {
|
|
Path dir = Path.of(tempDir);
|
|
if (Files.notExists(dir)) {
|
|
Files.createDirectories(dir);
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
public String saveFile(InputStream in, String filename, String mimeType) throws IOException {
|
|
ULID.Value id = ulid.nextValue();
|
|
|
|
FileMetadata meta = new FileMetadata();
|
|
meta.filename = filename;
|
|
meta.mimeType = mimeType;
|
|
byte[] metaBytes = objectMapper.writeValueAsBytes(meta);
|
|
|
|
Path filePath = getStoragePathForFile(id.toString());
|
|
Files.createDirectories(filePath.getParent());
|
|
try (var out = Files.newOutputStream(filePath)) {
|
|
// Write metadata first.
|
|
ByteBuffer metaBuffer = ByteBuffer.allocate(2 + metaBytes.length);
|
|
metaBuffer.putShort((short) metaBytes.length);
|
|
metaBuffer.put(metaBytes);
|
|
out.write(metaBuffer.array());
|
|
|
|
// Now write real data.
|
|
byte[] buffer = new byte[8192];
|
|
int readCount;
|
|
while ((readCount = in.read(buffer)) > 0) {
|
|
out.write(buffer, 0, readCount);
|
|
}
|
|
}
|
|
return id.toString();
|
|
}
|
|
|
|
public FileMetadata readMetadata(InputStream in) throws IOException {
|
|
ByteBuffer b = ByteBuffer.allocate(2);
|
|
int bytesRead = in.read(b.array());
|
|
if (bytesRead != 2) throw new IOException("Missing metadata length.");
|
|
short length = b.getShort();
|
|
b = ByteBuffer.allocate(length);
|
|
bytesRead = in.read(b.array());
|
|
if (bytesRead != length) throw new IOException("Metadata body does not equal length header.");
|
|
return objectMapper.readValue(b.array(), FileMetadata.class);
|
|
}
|
|
|
|
/**
|
|
* Streams the contents of a stored file to a client via the Http response.
|
|
* @param rawId The file's unique identifier.
|
|
* @param response The response to stream the content to.
|
|
*/
|
|
public void streamFile(String rawId, HttpServletResponse response) {
|
|
Path filePath = getStoragePathForFile(rawId);
|
|
if (!Files.exists(filePath)) {
|
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
}
|
|
|
|
try (var in = Files.newInputStream(filePath)) {
|
|
FileMetadata metadata = readMetadata(in);
|
|
response.setContentType(metadata.mimeType);
|
|
response.setContentLengthLong(Files.size(filePath));
|
|
}
|
|
|
|
ULID.Value id = ULID.parseULID(rawId);
|
|
Instant timestamp = Instant.ofEpochMilli(id.timestamp());
|
|
LocalDateTime ldt = LocalDateTime.ofInstant(timestamp, ZoneOffset.UTC);
|
|
response.setContentType(file.getMimeType());
|
|
response.setContentLengthLong(file.getSize());
|
|
try {
|
|
Path filePath = getStoragePathForFile(file);
|
|
try (var in = Files.newInputStream(filePath)) {
|
|
in.transferTo(response.getOutputStream());
|
|
}
|
|
} catch (IOException e) {
|
|
log.error("Failed to write file to response.", e);
|
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
|
|
public FileMetadataResponse getFileMetadata(String id) {
|
|
Path filePath = getStoragePathForFile(id);
|
|
if (!Files.exists(filePath)) {
|
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
}
|
|
try (var in = Files.newInputStream(filePath)) {
|
|
FileMetadata metadata = readMetadata(in);
|
|
return new FileMetadataResponse(
|
|
metadata.filename,
|
|
metadata.mimeType,
|
|
Files.size(filePath),
|
|
Files.getLastModifiedTime(filePath)
|
|
.toInstant().atOffset(ZoneOffset.UTC)
|
|
.toLocalDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
|
|
true
|
|
);
|
|
} catch (IOException e) {
|
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "IO error", e);
|
|
}
|
|
}
|
|
}
|