Basic implementation for notes app
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package de.nbscloud.files;
|
||||
|
||||
import de.nbscloud.files.api.FilesService.ContentContainer;
|
||||
import de.nbscloud.files.api.FilesService.ContentTree;
|
||||
import de.nbscloud.files.config.FilesConfig;
|
||||
import de.nbscloud.files.exception.FileSystemServiceException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
@@ -21,6 +22,7 @@ import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
@@ -117,6 +119,14 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
public void overwriteFile(Path path, byte[] content) {
|
||||
try {
|
||||
Files.write(path, content, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not overwrite file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path move(Path originalPath, Path newPath) {
|
||||
try {
|
||||
return Files.move(originalPath, newPath, StandardCopyOption.ATOMIC_MOVE);
|
||||
@@ -286,7 +296,7 @@ public class FileSystemService {
|
||||
|
||||
List<ContentContainer> list(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||
try {
|
||||
List<ContentContainer> contentList = Files.list(this.locationTracker.getCurrentLocation())
|
||||
List<ContentContainer> contentList = Files.list(startPath)
|
||||
.filter(path -> {
|
||||
boolean ok = Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) || Files.isDirectory(path);
|
||||
|
||||
@@ -319,22 +329,86 @@ public class FileSystemService {
|
||||
}
|
||||
}
|
||||
|
||||
ContentTree getTree(Path startPath, SortOrder sortOrder, Function<Path, Path> relativizer) {
|
||||
try {
|
||||
if (!Files.isDirectory(startPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ContentTree root = new ContentTree();
|
||||
|
||||
root.setName(startPath.getFileName().toString());
|
||||
root.setPath(relativizer.apply(startPath).toString());
|
||||
root.setDirectory(true);
|
||||
|
||||
final AtomicReference<ContentTree> current = new AtomicReference<>(root);
|
||||
|
||||
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (startPath.equals(dir)) {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
final ContentTree dirTree = new ContentTree();
|
||||
|
||||
dirTree.setName(dir.getFileName().toString());
|
||||
dirTree.setPath(relativizer.apply(dir).toString());
|
||||
dirTree.setDirectory(true);
|
||||
dirTree.setParent(current.get());
|
||||
|
||||
current.get().getSubTree().add(dirTree);
|
||||
current.set(dirTree);
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
final ContentTree fileTree = new ContentTree();
|
||||
|
||||
fileTree.setName(file.getFileName().toString());
|
||||
fileTree.setPath(relativizer.apply(file).toString());
|
||||
fileTree.setDirectory(false);
|
||||
fileTree.setParent(current.get());
|
||||
|
||||
current.get().getSubTree().add(fileTree);
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
current.set(current.get().getParent());
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
return root;
|
||||
} catch (IOException e) {
|
||||
throw new FileSystemServiceException("Could not list files", e);
|
||||
}
|
||||
}
|
||||
|
||||
public List<ContentContainer> list(SortOrder sortOrder) {
|
||||
return list(this.locationTracker.getCurrentLocation(), sortOrder, path -> this.locationTracker.getRelativeToBaseDir(path));
|
||||
}
|
||||
|
||||
public List<String> collectDirs(String sourceFile) {
|
||||
return collectDirs(this.locationTracker.resolve(sourceFile), this.locationTracker.getBaseDirPath(), path -> this.locationTracker.getRelativeToBaseDir(path), false);
|
||||
}
|
||||
|
||||
List<String> collectDirs(Path sourcePath, Path baseDir, Function<Path, Path> relativizer, boolean includeSource) {
|
||||
try {
|
||||
final List<String> resultList = new ArrayList<>();
|
||||
final Path sourcePath = this.locationTracker.resolve(sourceFile);
|
||||
final boolean sourceIsDir = Files.isDirectory(sourcePath);
|
||||
|
||||
Files.walkFileTree(this.locationTracker.getBaseDirPath(), new SimpleFileVisitor<Path>() {
|
||||
Files.walkFileTree(baseDir, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
super.visitFile(dir, attrs);
|
||||
|
||||
if (sourceIsDir && sourcePath.equals(dir)) {
|
||||
if (!includeSource && sourceIsDir && sourcePath.equals(dir)) {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
@@ -350,7 +424,7 @@ public class FileSystemService {
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
final String relPath = locationTracker.getRelativeToBaseDir(dir).toString();
|
||||
final String relPath = relativizer.apply(dir).toString();
|
||||
|
||||
resultList.add("/" + relPath);
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ public class FilesServiceImpl implements FilesService {
|
||||
private AppLocationTracker locationTracker;
|
||||
|
||||
private Path resolve(App app, Path path) {
|
||||
if(path.startsWith("/")) {
|
||||
path = path.subpath(0, path.getNameCount());
|
||||
}
|
||||
|
||||
return this.locationTracker.resolve(app.getId(), path);
|
||||
}
|
||||
|
||||
@@ -41,14 +45,19 @@ public class FilesServiceImpl implements FilesService {
|
||||
this.fileSystemService.createFile(resolve(app, path), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void overwriteFile(App app, Path path, byte[] content) {
|
||||
this.fileSystemService.overwriteFile(resolve(app, path), content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(App app, Path path) {
|
||||
this.fileSystemService.delete(resolve(app, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void get(App app, Path path) {
|
||||
this.fileSystemService.get(resolve(app, path));
|
||||
public byte[] get(App app, Path path) {
|
||||
return this.fileSystemService.get(resolve(app, path));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,4 +67,21 @@ public class FilesServiceImpl implements FilesService {
|
||||
|
||||
return this.fileSystemService.list(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentTree getTree(App app, Optional<Path> path) {
|
||||
final Path appPath = resolve(app, Path.of(""));
|
||||
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||
|
||||
return this.fileSystemService.getTree(p, FileSystemService.SortOrder.NATURAL, callbackPath -> appPath.relativize(callbackPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> collectDirs(App app, Optional<Path> path) {
|
||||
final Path appPath = resolve(app, Path.of(""));
|
||||
final Path p = path.map(tmpPath -> resolve(app, tmpPath)).orElse(appPath);
|
||||
|
||||
|
||||
return this.fileSystemService.collectDirs(p, appPath, callbackPath -> appPath.relativize(callbackPath), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.MessageHelper;
|
||||
import de.nbscloud.webcontainer.registry.AppRegistry;
|
||||
import de.nbscloud.webcontainer.shared.config.WebContainerSharedConfig;
|
||||
import org.apache.commons.io.input.ObservableInputStream;
|
||||
@@ -64,12 +65,8 @@ public class FilesController implements InitializingBean {
|
||||
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
|
||||
private final List<String> errors = new ArrayList<>();
|
||||
private final List<String> shareInfo = new ArrayList<>();
|
||||
private final List<String> infoMessages = new ArrayList<>();
|
||||
@Autowired
|
||||
private MessageHelper messageHelper;
|
||||
|
||||
@GetMapping("/files/browse/**")
|
||||
public String start(Model model, HttpServletRequest httpServletRequest, String sortOrder) {
|
||||
@@ -77,9 +74,7 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
updateLocation(httpServletRequest);
|
||||
|
||||
model.addAttribute("errors", getAndClear(this.errors));
|
||||
model.addAttribute("infoMessages", getAndClear(this.infoMessages));
|
||||
model.addAttribute("shareInfo", getAndClear(this.shareInfo));
|
||||
this.messageHelper.addAndClearAll(model);
|
||||
model.addAttribute("currentLocation", getCurrentLocationPrefixed());
|
||||
model.addAttribute("content", getContent(order));
|
||||
model.addAttribute("sortOrder", order);
|
||||
@@ -98,17 +93,17 @@ public class FilesController implements InitializingBean {
|
||||
this.locationTracker.resolve(filename),
|
||||
this.locationTracker.resolveTrash(filename));
|
||||
|
||||
this.infoMessages.add("nbscloud.files.delete.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not soft delete file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// Hard delete
|
||||
this.fileSystemService.delete(filename);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.delete.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.delete.success");
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
@@ -138,11 +133,11 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
this.fileSystemService.move(sourcePath, targetPath);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.rename.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.rename.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not rename file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
@@ -194,11 +189,11 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
this.fileSystemService.createFile(file.getOriginalFilename(), file.getBytes());
|
||||
|
||||
this.infoMessages.add("nbscloud.files.created.file.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.created.file.success");
|
||||
} catch (IOException | RuntimeException e) {
|
||||
logger.error("Could not upload file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +205,11 @@ public class FilesController implements InitializingBean {
|
||||
try {
|
||||
this.fileSystemService.createDirectory(dirName);
|
||||
|
||||
this.infoMessages.add("nbscloud.files.created.dir.success");
|
||||
this.messageHelper.addInfo("nbscloud.files.created.dir.success");
|
||||
} catch (RuntimeException e) {
|
||||
logger.error("Could not create dir", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
@@ -241,6 +236,7 @@ public class FilesController implements InitializingBean {
|
||||
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 -> ed.isBlank() ? null : ed)
|
||||
.map(ed -> LocalDate.parse(ed, DateTimeFormatter.ofPattern("yyyy-MM-dd")))
|
||||
.orElse(null);
|
||||
|
||||
@@ -254,11 +250,11 @@ public class FilesController implements InitializingBean {
|
||||
|
||||
this.fileSystemService.createFile(this.locationTracker.resolveShare(share.getUuid()), shareJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
this.shareInfo.add("/files/shares?shareUuid=" + share.getUuid());
|
||||
this.messageHelper.addShare("/files/shares?shareUuid=" + share.getUuid());
|
||||
} catch (RuntimeException | JsonProcessingException e) {
|
||||
logger.error("Could not share file", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
}
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
@@ -325,20 +321,12 @@ public class FilesController implements InitializingBean {
|
||||
} catch (FileSystemServiceException e) {
|
||||
logger.error("Could not generate gallery", e);
|
||||
|
||||
this.errors.add(e.getMessage());
|
||||
this.messageHelper.addError(e.getMessage());
|
||||
|
||||
return "redirect:/files/browse/" + this.locationTracker.getRelativeLocation();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getAndClear(List<String> source) {
|
||||
final List<String> retList = new ArrayList<>(source);
|
||||
|
||||
source.clear();
|
||||
|
||||
return retList;
|
||||
}
|
||||
|
||||
private List<ContentContainer> getContent(FileSystemService.SortOrder order) {
|
||||
final List<ContentContainer> contentList = this.fileSystemService.list(order);
|
||||
|
||||
|
||||
@@ -73,105 +73,6 @@
|
||||
width: 70em;
|
||||
}
|
||||
|
||||
#menu-container {
|
||||
padding-top: 2em;
|
||||
margin-left: 3em;
|
||||
}
|
||||
|
||||
.files-menu-icon {
|
||||
background: none !important;
|
||||
border: none;
|
||||
padding: 0 !important;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.files-menu-icon:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
#content-container {
|
||||
padding-top: unset !important;
|
||||
}
|
||||
|
||||
#create-dir-container > details > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#create-dir-container > details[open] > summary {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
#menu-container > div {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.create-dir-container-details-modal-content {
|
||||
padding: 0.5em;
|
||||
pointer-events: all;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.create-dir-container-details-modal {
|
||||
background: var(--background-color);
|
||||
filter: brightness(var(--background-brightness));
|
||||
border-radius: 0.3em;
|
||||
left: auto;
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.menu-spacer {
|
||||
width: 2em;
|
||||
}
|
||||
|
||||
.messageContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 2em;
|
||||
border: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 0.3em;
|
||||
padding: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.shareMessage {
|
||||
border-color: var(--good-color) !important;
|
||||
}
|
||||
|
||||
.infoMessage {
|
||||
border-color: var(--good-color) !important;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
border-color: var(--error-color) !important;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 450px) {
|
||||
#menu-container {
|
||||
padding-top: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.menu-spacer {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#gallery-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -13,22 +13,7 @@
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div th:replace="includes/header :: header"/>
|
||||
<div class="messageContainer" th:each="shareInfo : ${shareInfo}">
|
||||
<div th:if="${!shareInfo.isEmpty()}" class="shareMessage message">
|
||||
<span th:text="#{'nbscloud.files.share-message'}"/>
|
||||
<a th:href="@{${shareInfo}}" th:text="${shareInfo}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messageContainer" th:each="error : ${errors}">
|
||||
<div th:if="${!errors.isEmpty()}" class="errorMessage message">
|
||||
<span th:text="${error}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="messageContainer" th:each="infoMessage : ${infoMessages}">
|
||||
<div th:if="${!infoMessages.isEmpty()}" class="infoMessage message">
|
||||
<span th:text="#{${infoMessage}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div th:replace="includes/messages :: messages"/>
|
||||
<div th:replace="files/includes/menu :: menu"/>
|
||||
<div id="content-container">
|
||||
<table id="files-content-table">
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<div id="menu-container" th:fragment="menu">
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="upload-container">
|
||||
<div id="upload-container" class="menu-container">
|
||||
<form method="POST" action="#" th:action="@{/files/upload}" enctype="multipart/form-data">
|
||||
<!-- JavaScript required unfortunately -->
|
||||
<input id="upload-container-file-input" type="file" name="files" onchange="this.form.submit()"
|
||||
style="display: none;" multiple>
|
||||
<span id="upload-container-span-input" class="icon files-menu-icon"
|
||||
<span id="upload-container-span-input" class="icon menu-icon"
|
||||
onclick="document.getElementById('upload-container-file-input').click();"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="menu-spacer"></div>
|
||||
<div id="create-dir-container">
|
||||
<div id="create-dir-container" class="menu-container">
|
||||
<details>
|
||||
<summary>
|
||||
<span id="create-dir-container-span-input" class="icon files-menu-icon"></span>
|
||||
<span id="create-dir-container-span-input" class="icon menu-icon"></span>
|
||||
<div class="create-dir-container-details-modal-overlay"></div>
|
||||
</summary>
|
||||
<div class="create-dir-container-details-modal">
|
||||
<div class="create-dir-container-details-modal-content">
|
||||
<div class="menu-modal">
|
||||
<div class="menu-modal-content">
|
||||
<form method="POST" action="#" th:action="@{/files/createDir}" enctype="multipart/form-data">
|
||||
<input id="create-dir-container-dir-name" type="text" name="dirName" />
|
||||
<input type="submit" value="Create"/>
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="menu-spacer"></div>
|
||||
<div>
|
||||
<a id="gallery-link" th:href="@{/files/gallery}">
|
||||
<span class="icon files-menu-icon"></span>
|
||||
<span class="icon menu-icon"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user