1
0

Basic implementation for notes app

This commit is contained in:
2022-08-25 17:14:37 +02:00
parent a0239ecda6
commit 2ab4497bd1
22 changed files with 671 additions and 171 deletions

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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">

View File

@@ -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();">&#xe9fc;</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">&#xe2cc;</span>
<span id="create-dir-container-span-input" class="icon menu-icon">&#xe2cc;</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">&#xe3b6;</span>
<span class="icon menu-icon">&#xe3b6;</span>
</a>
</div>
</div>