diff --git a/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java index 2ced2a915ec557c8f8ca9d4b888916f034585c16..fcc19d6ba613e21b818d626c61492b14c2a314a7 100644 --- a/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java +++ b/src/main/java/it/inaf/ia2/transfer/controller/PutFileController.java @@ -6,6 +6,7 @@ package it.inaf.ia2.transfer.controller; import it.inaf.ia2.transfer.exception.FileNotFoundException; +import it.inaf.ia2.transfer.exception.InsufficientStorageException; import it.inaf.ia2.transfer.exception.InvalidArgumentException; import it.inaf.ia2.transfer.persistence.model.FileInfo; import it.inaf.ia2.transfer.persistence.FileDAO; @@ -64,14 +65,24 @@ public class PutFileController extends FileController { Optional<FileInfo> optFileInfo = fileDAO.getFileInfo(path); if (optFileInfo.isPresent()) { - try (InputStream in = file != null ? file.getInputStream() : request.getInputStream()) { - FileInfo fileInfo = optFileInfo.get(); + FileInfo fileInfo = optFileInfo.get(); + + String parentPath = fileInfo.getVirtualPath().substring(0, fileInfo.getVirtualPath().lastIndexOf("/")); + Long remainingQuota = fileDAO.getRemainingQuota(parentPath); + + // if MultipartFile provides file size it is possible to check + // quota limit before reading the stream + if (remainingQuota != null && file != null && file.getSize() > remainingQuota) { + throw new InsufficientStorageException(fileInfo.getVirtualPath()); + } + + if (file != null) { + fileInfo.setContentType(file.getContentType()); + } + fileInfo.setContentEncoding(contentEncoding); - if (file != null) { - fileInfo.setContentType(file.getContentType()); - } - fileInfo.setContentEncoding(contentEncoding); - storeGenericFile(fileInfo, in, jobId); + try (InputStream in = file != null ? file.getInputStream() : request.getInputStream()) { + storeGenericFile(fileInfo, in, jobId, remainingQuota); } catch (IOException | NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } @@ -81,7 +92,7 @@ public class PutFileController extends FileController { }, jobId); } - private void storeGenericFile(FileInfo fileInfo, InputStream is, String jobId) throws IOException, NoSuchAlgorithmException { + private void storeGenericFile(FileInfo fileInfo, InputStream is, String jobId, Long remainingQuota) throws IOException, NoSuchAlgorithmException { File file = new File(fileInfo.getOsPath()); @@ -112,6 +123,13 @@ public class PutFileController extends FileController { } Long fileSize = Files.size(file.toPath()); + + // Quota limit is checked again to handle cases where MultipartFile is not used + if (remainingQuota != null && fileSize > remainingQuota) { + file.delete(); + throw new InsufficientStorageException(fileInfo.getVirtualPath()); + } + String md5Checksum = makeMD5Checksum(file); fileDAO.updateFileAttributes(fileInfo.getNodeId(), diff --git a/src/main/java/it/inaf/ia2/transfer/exception/InsufficientStorageException.java b/src/main/java/it/inaf/ia2/transfer/exception/InsufficientStorageException.java new file mode 100644 index 0000000000000000000000000000000000000000..9e2de5996e140d7decfce1d4014b7e2c2f27358a --- /dev/null +++ b/src/main/java/it/inaf/ia2/transfer/exception/InsufficientStorageException.java @@ -0,0 +1,18 @@ +/* + * This file is part of vospace-file-service + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.ia2.transfer.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE) +public class InsufficientStorageException extends JobException { + + public InsufficientStorageException(String path) { + super(Type.FATAL, "Quota Exceeded"); + setErrorDetail("QuotaExceeded Path: " + path); + } +} diff --git a/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java index 9fe0554eb418098a137e90154ea8c54c0fd0557a..72f820bc328b5cacce5e71b70620b3be5c260891 100644 --- a/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java +++ b/src/test/java/it/inaf/ia2/transfer/controller/PutFileControllerTest.java @@ -5,14 +5,18 @@ */ package it.inaf.ia2.transfer.controller; +import it.inaf.ia2.transfer.exception.InsufficientStorageException; import it.inaf.ia2.transfer.persistence.model.FileInfo; import it.inaf.ia2.transfer.persistence.FileDAO; import it.inaf.ia2.transfer.persistence.JobDAO; +import java.io.ByteArrayInputStream; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Optional; import java.util.UUID; +import javax.servlet.ServletInputStream; import net.ivoa.xml.uws.v1.ExecutionPhase; import org.assertj.core.util.Files; import org.junit.jupiter.api.AfterAll; @@ -22,6 +26,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import org.mockito.Mockito; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -70,6 +76,8 @@ public class PutFileControllerTest { @Test public void putGenericFile() throws Exception { + when(fileDao.getRemainingQuota(any())).thenReturn(null); + String randomFileName = UUID.randomUUID().toString(); createBaseFileInfo(randomFileName); @@ -99,6 +107,8 @@ public class PutFileControllerTest { private void putGenericFileWithNameConflict(String name1, String name2, String name3) throws Exception { + when(fileDao.getRemainingQuota(any())).thenReturn(null); + createBaseFileInfo(name1); MockMultipartFile fakeFile = new MockMultipartFile("file", "test.txt", "text/plain", "content".getBytes()); @@ -143,6 +153,8 @@ public class PutFileControllerTest { @Test public void putGenericFileWithJobId() throws Exception { + when(fileDao.getRemainingQuota(any())).thenReturn(null); + when(jobDAO.isJobExisting("pippo10")).thenReturn(false); when(jobDAO.isJobExisting("pippo5")).thenReturn(true); @@ -221,19 +233,73 @@ public class PutFileControllerTest { verify(jobDAO, times(1)).setJobError(eq("abcdef"), any()); } + + @Test + public void testQuotaExceededMultipart() throws Exception { + + when(fileDao.getRemainingQuota(eq("/path/to"))).thenReturn(0l); + + createBaseFileInfo(); + + MockMultipartFile fakeFile = new MockMultipartFile("file", "test.txt", null, "content".getBytes()); + + Exception ex = mockMvc.perform(putMultipart("/path/to/test.txt") + .file(fakeFile)) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andReturn().getResolvedException(); + + verify(fileDao, times(1)).getRemainingQuota(eq("/path/to")); + + assertTrue(ex instanceof InsufficientStorageException); + } + + @Test + public void testQuotaExceededStream() throws Exception { + + when(fileDao.getRemainingQuota(eq("/path/to"))).thenReturn(0l); + + createBaseFileInfo(); + + MockHttpServletRequestBuilder streamBuilder = put("/path/to/test.txt"); + streamBuilder.with(new RequestPostProcessor() { + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + MockHttpServletRequest spyRequest = spy(request); + ByteArrayInputStream bais = new ByteArrayInputStream("some data".getBytes()); + ServletInputStream sis = mock(ServletInputStream.class); + try { + when(sis.transferTo(any())).thenAnswer(i -> bais.transferTo(i.getArgument(0))); + } catch (IOException ex) { + } + Mockito.doReturn(sis).when(spyRequest).getInputStream(); + return spyRequest; + } + }); + + Exception ex = mockMvc.perform(streamBuilder) + .andDo(print()) + .andExpect(status().is5xxServerError()) + .andReturn().getResolvedException(); + + verify(fileDao, times(1)).getRemainingQuota(eq("/path/to")); + + assertTrue(ex instanceof InsufficientStorageException); + } private FileInfo createBaseFileInfo() { String randomFileName = UUID.randomUUID().toString(); return createBaseFileInfo(randomFileName); } - - private FileInfo createBaseFileInfo(String fileName) { + + private FileInfo createBaseFileInfo(String fileName) { FileInfo fileInfo = new FileInfo(); fileInfo.setOsPath(getTestFilePath(fileName)); + fileInfo.setVirtualPath("/path/to/" + fileName); fileInfo.setPublic(false); - + when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo)); - + return fileInfo; }