Skip to content

Commit 2bc3503

Browse files
committed
file download and other fixes
1 parent de12028 commit 2bc3503

File tree

8 files changed

+94
-5
lines changed

8 files changed

+94
-5
lines changed
Lines changed: 1 addition & 0 deletions
Loading

src/main/frontend/components/BreadCrumbs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export default function BreadCrumbs({path}: BreadcrumbProps) {
2424
const breadcrumbNavigateRoot = () => navigate("/browser");
2525

2626
return <>
27-
<div className="flex flex-row items-start gap-s pb-m">
27+
<div className="flex flex-row flex-wrap items-start gap-s pb-m">
2828
<span className="breadcrumb" title="/">
2929
<Icon src={folderIcon} onClick={() => breadcrumbNavigateRoot()}/>
3030
<span className="ml-s text-secondary">/</span>

src/main/frontend/views/browser/{...path}.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@ import {ViewConfig} from "@vaadin/hilla-file-router/types.js";
22
import {FileBrowserService} from "Frontend/generated/endpoints";
33
import {useSignal} from "@vaadin/hilla-react-signals";
44
import FileItem from "Frontend/generated/pro/homedns/filebrowser/model/FileItem";
5-
import {Grid, GridSortColumn, Icon} from "@vaadin/react-components";
5+
import {Grid, GridColumn, GridSortColumn, Icon} from "@vaadin/react-components";
66
import {useEffect} from "react";
77

88
import folderIcon from "@icons/icons8-folder.svg?url";
99
import fileIcon from "@icons/icons8-file.svg?url";
10+
import downloadImage from "@images/icons8-download-from-the-cloud.svg?url";
1011

1112
import {useNavigate, useParams} from "react-router";
1213
import BreadCrumbs from "Frontend/components/BreadCrumbs";
1314
import {formatDate} from "Frontend/util/dateFormat";
1415

16+
1517
export const config: ViewConfig = {
1618
title: 'File Browser',
1719
loginRequired: true,
@@ -37,7 +39,7 @@ export default function FileBrowserView() {
3739
<BreadCrumbs path={wildcardPath}/>
3840

3941
<Grid items={fileItems.value}>
40-
<GridSortColumn path="displayName" header="Name" renderer={
42+
<GridSortColumn resizable path="displayName" header="Name" renderer={
4143
({item}) => (
4244
<div className="flex gap-s items-center"
4345
onClick={() => visitItem(item)}
@@ -47,16 +49,28 @@ export default function FileBrowserView() {
4749
</div>
4850
)}
4951
/>
50-
<GridSortColumn path="fileSize" header="Size" renderer={
52+
<GridSortColumn resizable path="fileSize" header="Size" renderer={
5153
({item}) => (
5254
<span className="text-s text-secondary">{item.fileSize}</span>
5355
)}
5456
/>
55-
<GridSortColumn path="lastModifiedOn" header="Last Modified On" renderer={
57+
<GridSortColumn resizable path="lastModifiedOn" header="Last Modified On" renderer={
5658
({item}) => (
5759
<span className="text-s text-secondary">{formatDate(item.lastModifiedOn)}</span>
5860
)}
5961
/>
62+
<GridColumn resizable frozenToEnd renderer={
63+
({item}) => (
64+
<>
65+
{item.directory ?
66+
<></> :
67+
<a href={`/download/${item.path}`} download>
68+
<img src={downloadImage} width="32" alt="Vaadin logo"/>
69+
</a>
70+
}
71+
</>
72+
)}
73+
/>
6074
</Grid>
6175
</>;
6276
}

src/main/java/pro/homedns/filebrowser/config/SecurityConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ public class SecurityConfig extends VaadinWebSecurity {
4040
@Override
4141
protected void configure(final HttpSecurity http) throws Exception {
4242
http.oauth2Login(Customizer.withDefaults());
43+
44+
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/download/**").authenticated());
45+
4346
super.configure(http);
4447
}
4548

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package pro.homedns.filebrowser.hilla;
2+
3+
import java.io.IOException;
4+
5+
import lombok.extern.log4j.Log4j2;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.core.io.ByteArrayResource;
8+
import org.springframework.core.io.Resource;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RequestMethod;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import pro.homedns.filebrowser.service.FileService;
16+
17+
@Log4j2
18+
@RestController
19+
public class FileDownloadEndpoint {
20+
21+
private final FileService fileService;
22+
23+
@Autowired
24+
public FileDownloadEndpoint(final FileService fileService) {
25+
this.fileService = fileService;
26+
}
27+
28+
@RequestMapping(value = "/download/{*filePath}", method = RequestMethod.GET)
29+
public ResponseEntity<Resource> downloadFile(@PathVariable String filePath) {
30+
final var normalizedFilePath = filePath.replaceFirst("^/", "");
31+
32+
try {
33+
final var fileDownloadData = fileService.downloadFile(normalizedFilePath);
34+
final var file = new ByteArrayResource(fileDownloadData.content());
35+
36+
return ResponseEntity.ok()
37+
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileDownloadData.name() + "\"")
38+
.body(file);
39+
} catch (IOException | IllegalArgumentException | SecurityException ex) {
40+
log.error(ex.getMessage(), ex);
41+
}
42+
43+
return ResponseEntity.notFound().build();
44+
}
45+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package pro.homedns.filebrowser.model;
2+
3+
public record FileDownloadData(String name, byte[] content) {
4+
}

src/main/java/pro/homedns/filebrowser/service/FileService.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.springframework.beans.factory.annotation.Autowired;
1313
import org.springframework.stereotype.Service;
1414
import pro.homedns.filebrowser.config.ApplicationProperties;
15+
import pro.homedns.filebrowser.model.FileDownloadData;
1516
import pro.homedns.filebrowser.model.FileItem;
1617

1718
@Log4j2
@@ -61,4 +62,22 @@ public List<FileItem> getItems(final String pathString) {
6162
}
6263
return Collections.emptyList();
6364
}
65+
66+
public FileDownloadData downloadFile(String pathString) throws IOException {
67+
final var rootPath = applicationProperties.root();
68+
final var normalizedPath = rootPath.resolve(pathString).normalize();
69+
70+
if (!normalizedPath.startsWith(rootPath)) {
71+
throw new SecurityException("Invalid file path: path traversal attempt detected.");
72+
}
73+
74+
if (normalizedPath.toFile().isDirectory()) {
75+
throw new IllegalArgumentException("Invalid file path: directory download not supported.");
76+
}
77+
78+
final var content = Files.readAllBytes(normalizedPath);
79+
final var name = normalizedPath.getFileName().toString();
80+
81+
return new FileDownloadData(name, content);
82+
}
6483
}

tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
"@icons/*": [
3434
"assets/icons/*"
3535
],
36+
"@images/*": [
37+
"assets/images/*"
38+
],
3639
"Frontend/*": [
3740
"*"
3841
]

0 commit comments

Comments
 (0)