diff --git a/files/pom.xml b/files/pom.xml
index ad1aabe..fe299b8 100644
--- a/files/pom.xml
+++ b/files/pom.xml
@@ -44,6 +44,11 @@
org.apache.commons
commons-lang3
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
diff --git a/files/src/main/java/de/nbscloud/files/FileSystemService.java b/files/src/main/java/de/nbscloud/files/FileSystemService.java
index 268068e..3e6c6cc 100644
--- a/files/src/main/java/de/nbscloud/files/FileSystemService.java
+++ b/files/src/main/java/de/nbscloud/files/FileSystemService.java
@@ -127,6 +127,15 @@ public class FileSystemService {
}
}
+ public boolean delete(Path path) {
+ try {
+ // TODO does only delete dirs if they are empty - but maybe we want that?
+ return Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new FileSystemServiceException("Could not delete file", e);
+ }
+ }
+
public byte[] get(Path path) {
try {
return Files.readAllBytes(path);
diff --git a/files/src/main/java/de/nbscloud/files/LocationTracker.java b/files/src/main/java/de/nbscloud/files/LocationTracker.java
index cbb2cb2..824d928 100644
--- a/files/src/main/java/de/nbscloud/files/LocationTracker.java
+++ b/files/src/main/java/de/nbscloud/files/LocationTracker.java
@@ -67,12 +67,30 @@ public class LocationTracker {
}
public void setCurrentLocation(String navigateTo) {
- validate(navigateTo);
+ validate_internal(navigateTo);
this.currentLocation = this.baseDirPath.resolve(navigateTo);
}
- private void validate(String navigateTo) {
+ public void validate(String path) {
+ // Absolut paths are allowed, however, they have to be under baseDir
+
+ if(path == null) {
+ throw new IllegalStateException("Null");
+ }
+
+ if(path.contains("..")) {
+ throw new IllegalStateException("Relative path");
+ }
+
+ final Path tmpPath = Path.of(path);
+
+ if(!tmpPath.normalize().startsWith(this.baseDirPath)) {
+ throw new IllegalStateException("Illegal path: " + path);
+ }
+ }
+
+ private void validate_internal(String navigateTo) {
if(navigateTo == null) {
throw new IllegalStateException("Null");
}
@@ -97,25 +115,25 @@ public class LocationTracker {
}
public Path resolve(String name) {
- validate(name);
+ validate_internal(name);
return this.currentLocation.resolve(name).normalize();
}
public Path resolveToBaseDir(String name) {
- validate(name);
+ validate_internal(name);
return this.baseDirPath.resolve(name).normalize();
}
public Path resolveShare(String shareUuid) {
- validate(shareUuid);
+ validate_internal(shareUuid);
return this.baseDirPath.resolve(this.filesConfig.getSharesName()).resolve(shareUuid);
}
public Path resolveTrash(String name) {
- validate(name);
+ validate_internal(name);
return this.baseDirPath.resolve(this.filesConfig.getTrashBinName()).resolve(name);
}
diff --git a/files/src/main/java/de/nbscloud/files/Share.java b/files/src/main/java/de/nbscloud/files/Share.java
new file mode 100644
index 0000000..fca81db
--- /dev/null
+++ b/files/src/main/java/de/nbscloud/files/Share.java
@@ -0,0 +1,42 @@
+package de.nbscloud.files;
+
+import java.time.LocalDate;
+
+public class Share {
+ private String uuid;
+ private boolean oneTime;
+ private LocalDate expiryDate;
+ private String path;
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public boolean isOneTime() {
+ return oneTime;
+ }
+
+ public void setOneTime(boolean oneTime) {
+ this.oneTime = oneTime;
+ }
+
+ public LocalDate getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(LocalDate expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+}
diff --git a/files/src/main/java/de/nbscloud/files/controller/FilesController.java b/files/src/main/java/de/nbscloud/files/controller/FilesController.java
index e68fcfb..228234c 100644
--- a/files/src/main/java/de/nbscloud/files/controller/FilesController.java
+++ b/files/src/main/java/de/nbscloud/files/controller/FilesController.java
@@ -1,12 +1,17 @@
package de.nbscloud.files.controller;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import de.nbscloud.files.FileSystemService;
import de.nbscloud.files.LocationTracker;
+import de.nbscloud.files.Share;
import de.nbscloud.files.config.FilesConfig;
import de.nbscloud.files.exception.FileSystemServiceException;
import de.nbscloud.files.form.RenameForm;
+import de.nbscloud.files.form.ShareForm;
import de.nbscloud.webcontainer.registry.AppRegistry;
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
+import org.apache.commons.io.input.ObservableInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
@@ -25,15 +30,19 @@ import org.springframework.web.util.UriUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.swing.text.html.Option;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Path;
import java.time.Instant;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
@Controller
@@ -51,6 +60,8 @@ public class FilesController implements InitializingBean {
private FileSystemService fileSystemService;
@Autowired
private AppRegistry appRegistry;
+ @Autowired
+ private ObjectMapper mapper;
// We have to temporarily store messages as we redirect: in the manipulation methods
// so everything we add to the model will be gone
@@ -107,6 +118,7 @@ public class FilesController implements InitializingBean {
model.addAttribute("form", new RenameForm(filename, getCurrentLocation(), filename));
model.addAttribute("targetDirs", this.fileSystemService.collectDirs(filename));
model.addAttribute("apps", this.appRegistry.getAll());
+ model.addAttribute("filename", filename);
this.webContainerSharedConfig.addDefaults(model);
return "files/rename";
@@ -135,7 +147,7 @@ public class FilesController implements InitializingBean {
}
@GetMapping("/files/download")
- public ResponseEntity downloadFile(String filename) {
+ public ResponseEntity downloadFile(String filename) {
// TODO download of directories via .zip? Also needs to be streaming
try {
@@ -147,7 +159,7 @@ public class FilesController implements InitializingBean {
} catch (RuntimeException e) {
logger.error("Could not get file", e);
- return ResponseEntity.internalServerError().body(null);
+ return ResponseEntity.internalServerError().body(":(");
}
}
@@ -183,41 +195,82 @@ public class FilesController implements InitializingBean {
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
}
- @GetMapping("/files/share")
- public String share(String filename) {
+ @GetMapping("files/share")
+ public String share(Model model, String filename) {
+ model.addAttribute("currentLocation", getCurrentLocationPrefixed());
+ model.addAttribute("form", new ShareForm(filename, false, LocalDate.now().plusDays(1)));
+ model.addAttribute("apps", this.appRegistry.getAll());
+ model.addAttribute("filename", filename);
+ this.webContainerSharedConfig.addDefaults(model);
+
+ return "files/share";
+ }
+
+ @PostMapping("/files/doShare")
+ public String doShare(@RequestParam("filename") String filename,
+ @RequestParam(value = "oneTime", defaultValue = "false") boolean oneTime,
+ @RequestParam(value = "expiryDate", required = false) String expiryDateString
+ ) {
final Path filePath = this.locationTracker.getRelativeToBaseDir(this.locationTracker.resolve(filename));
final String shareUuid = UUID.randomUUID().toString();
+ final Share share = new Share();
+ // The format is always "yyyy-MM-dd", see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
+ final LocalDate expiryDate = Optional.ofNullable(expiryDateString)
+ .map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
+ .orElse(null);
+
+ share.setUuid(shareUuid);
+ share.setPath(filePath.toString());
+ share.setExpiryDate(expiryDate);
+ share.setOneTime(oneTime);
try {
- this.fileSystemService.createFile(this.locationTracker.resolveShare(shareUuid), filePath.toString().getBytes(StandardCharsets.UTF_8));
+ final String shareJson = mapper.writeValueAsString(share);
- this.shareInfo.add("/files/shares?shareUuid=" + shareUuid);
- } catch (RuntimeException e) {
+ this.fileSystemService.createFile(this.locationTracker.resolveShare(share.getUuid()), shareJson.getBytes(StandardCharsets.UTF_8));
+
+ this.shareInfo.add("/files/shares?shareUuid=" + share.getUuid());
+ } catch (RuntimeException | JsonProcessingException e) {
logger.error("Could not share file", e);
this.errors.add(e.getMessage());
}
-
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
}
@GetMapping("files/shares")
public ResponseEntity shares(String shareUuid) {
try {
- final String sharedFile = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
- final Path sharedFilePath = this.locationTracker.resolveToBaseDir(sharedFile);
+ final String shareJson = new String(this.fileSystemService.get(this.locationTracker.resolveShare(shareUuid)));
+ final Share share = mapper.readValue(shareJson, Share.class);
+ final Path sharedFilePath = this.locationTracker.resolveToBaseDir(share.getPath());
final String filename = sharedFilePath.getFileName().toString();
+ final Optional optExpiryDate = Optional.ofNullable(share.getExpiryDate());
+ final boolean expired = optExpiryDate.map(expiryDate -> LocalDate.now().isAfter(expiryDate)).orElse(false);
+
+ if (expired) {
+ fileSystemService.delete(locationTracker.resolveShare(shareUuid));
+
+ return ResponseEntity.status(410).body("Share expired!");
+ }
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.header(HttpHeaders.CONTENT_TYPE, this.fileSystemService.getMimeType(filename))
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(this.fileSystemService.getSize(filename)))
- .body(new InputStreamResource(this.fileSystemService.stream(filename)));
- } catch (RuntimeException e) {
+ .body(new InputStreamResource(new ObservableInputStream(this.fileSystemService.stream(filename), new ObservableInputStream.Observer() {
+ @Override
+ public void closed() throws IOException {
+ if (share.isOneTime()) {
+ fileSystemService.delete(locationTracker.resolveShare(shareUuid));
+ }
+ }
+ })));
+ } catch (RuntimeException | JsonProcessingException e) {
logger.error("Could not get shared file", e);
- return ResponseEntity.internalServerError().body(null);
+ return ResponseEntity.internalServerError().body(":(");
}
}
diff --git a/files/src/main/java/de/nbscloud/files/form/ShareForm.java b/files/src/main/java/de/nbscloud/files/form/ShareForm.java
new file mode 100644
index 0000000..25cf532
--- /dev/null
+++ b/files/src/main/java/de/nbscloud/files/form/ShareForm.java
@@ -0,0 +1,39 @@
+package de.nbscloud.files.form;
+
+import java.time.LocalDate;
+
+public class ShareForm {
+ private String filename;
+ private boolean oneTime;
+ private LocalDate expiryDate;
+
+ public ShareForm(String filename, boolean oneTime, LocalDate expiryDate) {
+ this.filename = filename;
+ this.oneTime = oneTime;
+ this.expiryDate = expiryDate;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public void setFilename(String filename) {
+ this.filename = filename;
+ }
+
+ public boolean isOneTime() {
+ return oneTime;
+ }
+
+ public void setOneTime(boolean oneTime) {
+ this.oneTime = oneTime;
+ }
+
+ public LocalDate getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(LocalDate expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+}
diff --git a/files/src/main/resources/i18n/files_messages.properties b/files/src/main/resources/i18n/files_messages.properties
index 9247cbf..6fd8be1 100644
--- a/files/src/main/resources/i18n/files_messages.properties
+++ b/files/src/main/resources/i18n/files_messages.properties
@@ -6,18 +6,22 @@ nbscloud.files.files-content-table.table-header.lastmodified=Last modified
nbscloud.files.files-content-table.table-header.actions=Actions
nbscloud.files-content-table.table.actions.delete=Delete
-nbscloud.files-content-table.table.actions.rename=Rename/move
+nbscloud.files-content-table.table.actions.rename=Rename/move...
nbscloud.files-content-table.table.actions.download=Download
-nbscloud.files-content-table.table.actions.share=Share
+nbscloud.files-content-table.table.actions.share=Share...
nbscloud.files-content-table.table.actions.preview=Preview
nbscloud.files.rename.title=nbscloud - files\: rename/move \u0020
-
nbscloud.files.rename.label.filename=Filename\:
nbscloud.files.rename.label.targetDir=Target directory\:
-
nbscloud.files.rename.submit=Rename/move
+nbscloud.files.share.title=nbscloud - files\: share \u0020
+nbscloud.files.share.label.filename=Filename\:
+nbscloud.files.share.label.onetime=One time\:
+nbscloud.files.share.label.expirydate=Expiry date\:
+nbscloud.files.share.submit=Share
+
nbscloud.files.share-message=Shared file\:\u0020
nbscloud.files.delete.success=File deleted
diff --git a/files/src/main/resources/i18n/files_messages_de_DE.properties b/files/src/main/resources/i18n/files_messages_de_DE.properties
index a0f2eb6..e9d5f70 100644
--- a/files/src/main/resources/i18n/files_messages_de_DE.properties
+++ b/files/src/main/resources/i18n/files_messages_de_DE.properties
@@ -6,18 +6,22 @@ nbscloud.files.files-content-table.table-header.lastmodified=Zuletzt ge\u00E4nde
nbscloud.files.files-content-table.table-header.actions=Aktionen
nbscloud.files-content-table.table.actions.delete=L\u00F6schen
-nbscloud.files-content-table.table.actions.rename=Umbenennen/verschieben
+nbscloud.files-content-table.table.actions.rename=Umbenennen/verschieben...
nbscloud.files-content-table.table.actions.download=Download
-nbscloud.files-content-table.table.actions.share=Teilen
+nbscloud.files-content-table.table.actions.share=Teilen...
nbscloud.files-content-table.table.actions.preview=Vorschau
nbscloud.files.rename.title=nbscloud - Dateien\: umbenennen/verschieben \u0020
-
nbscloud.files.rename.label.filename=Dateiname\:
nbscloud.files.rename.label.targetDir=Zielverzeichnis\:
-
nbscloud.files.rename.submit=Umbenennen/verschieben
+nbscloud.files.share.title=nbscloud - Dateien\: teilen \u0020
+nbscloud.files.share.label.filename=Dateiname\:
+nbscloud.files.share.label.onetime=Einmaliger Abruf\:
+nbscloud.files.share.label.expirydate=Ablaufdatum\:
+nbscloud.files.share.submit=Teilen
+
nbscloud.files.share-message=Datei geteilt\:\u0020
nbscloud.files.delete.success=Datei gel\u00F6scht
diff --git a/files/src/main/resources/static/css/files_main.css b/files/src/main/resources/static/css/files_main.css
index f563cb5..e85e71f 100644
--- a/files/src/main/resources/static/css/files_main.css
+++ b/files/src/main/resources/static/css/files_main.css
@@ -48,17 +48,19 @@
cursor: pointer;
}
-#rename-form {
+#rename-form,
+#share-form {
margin-top: 3em;
margin-left: 3em;
}
-#rename-form * {
+#rename-form *,
+#share-form * {
display: block;
margin-top: 1em;
box-sizing: border-box;
}
-#rename-form > select > * {
+#rename-form > select > *{
margin-top: unset;
}
@@ -66,7 +68,8 @@
width: 70em;
}
-#rename-form > input[type=text] {
+#rename-form > input[type=text],
+#share-form > input[type=text] {
width: 70em;
}
diff --git a/files/src/main/resources/templates/files/share.html b/files/src/main/resources/templates/files/share.html
new file mode 100644
index 0000000..fbfe0ab
--- /dev/null
+++ b/files/src/main/resources/templates/files/share.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file