Merge pull request 'frontend' (#5) from frontend into main
Reviewed-on: https://git.walzen665.de/Walzen665/fileshare-service/pulls/5
This commit is contained in:
commit
4bcfa0500c
@ -1,6 +1,6 @@
|
|||||||
<div class="container mx-auto p-4 mt-4">
|
<div class="container mx-auto p-4 mt-4">
|
||||||
<h1 class="text-4xl font-bold text-center text-gray-800 mb-10">Upload Your File</h1>
|
<h1 class="text-4xl font-bold text-center text-gray-800 mb-5">Upload Your File</h1>
|
||||||
<div class="bg-white shadow-lg rounded-lg p-6">
|
<div class="bg-white shadow-lg rounded-lg lg:p-6 p-3">
|
||||||
<div *ngIf="!uploadStarted" class="flex flex-col items-center justify-center lg:p-5">
|
<div *ngIf="!uploadStarted" class="flex flex-col items-center justify-center lg:p-5">
|
||||||
<!-- File Drop Area -->
|
<!-- File Drop Area -->
|
||||||
<div
|
<div
|
||||||
@ -52,15 +52,66 @@
|
|||||||
<p class="text-gray-700 mt-4">Uploading {{ fileToUpload.name }}...</p>
|
<p class="text-gray-700 mt-4">Uploading {{ fileToUpload.name }}...</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Upload Finished -->
|
<!-- Upload Finished -->
|
||||||
<div *ngIf="uploadFinished" class="flex flex-col items-center justify-center animate-in fade-in duration-1000">
|
<div *ngIf="uploadFinished" class="flex flex-col items-center justify-center lg:p-6 animate-fade-in-up">
|
||||||
<img class="w-16 mb-3" src="./assets/circle-check-solid.svg">
|
<div class="flex items-center justify-center rounded-full p-2 border-4 border-indigo-500">
|
||||||
<p class="text-gray-700 mb-3">File uploaded successfully!</p>
|
<img class="w-16" [ngClass]="{'animate-in spin-in-1 duration-1000': uploadFinished}" src="./assets/circle-check-solid.svg" alt="Success">
|
||||||
<button class="btn btn-primary w-full max-w-xs">Upload Another File</button>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold mt-3">File uploaded successfully!</h3>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap mt-5 mb-5" *ngIf="uploadData && fileUrls">
|
||||||
|
<div class="w-full px-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-center">
|
||||||
|
<div class="text-gray-600 lg:text-right">File Name:</div>
|
||||||
|
<div>{{uploadData.fileName}}</div>
|
||||||
|
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">Is password protected:</div>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-primary" [checked]="uploadData.passwordProtected" (click)="$event.preventDefault()"/>
|
||||||
|
<span>{{uploadData.passwordProtected ? 'Yes' : 'No'}}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">Is single download:</div>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-primary" [checked]="uploadData.singleDownload" (click)="$event.preventDefault()"/>
|
||||||
|
<span>{{uploadData.singleDownload ? 'Yes' : 'No'}}</span>
|
||||||
|
</label>
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">Download Password:</div>
|
||||||
|
<div>{{uploadData.password || 'N/A'}}</div>
|
||||||
|
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">File ID:</div>
|
||||||
|
<div>{{uploadData.fileId}}</div>
|
||||||
|
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">Download URL:</div>
|
||||||
|
<div><a href="{{fileUrls.downloadUrl}}" target="_blank">{{fileUrls.downloadUrl}}</a></div>
|
||||||
|
|
||||||
|
<div class="text-gray-600 lg:text-right mt-1 lg:mt-0">File Status URL:</div>
|
||||||
|
<div><a href="{{fileUrls.statusUrl}}" target="_blank">{{fileUrls.statusUrl}}</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Your file is now securely stored on our servers.</p>
|
||||||
|
|
||||||
|
<div class="mt-4 flex flex-col w-full items-center">
|
||||||
|
<button class="btn btn-success w-full max-w-xs text-white" (click)="copyUrlToClipboard(this.fileUrls?.downloadUrl)">
|
||||||
|
<div *ngIf="!urlCopied">Copy Download URL</div>
|
||||||
|
<div *ngIf="urlCopied"><img width="20" src="./assets/check-solid.svg" alt="kk"></div>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary mt-2 w-full max-w-xs" routerLink="/home">Home</button>
|
||||||
|
<button class="btn btn-outline btn-secondary mt-2 w-full max-w-xs" disabled>Delete this file</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Invisible SVGs to prevent lazy loading -->
|
<!-- Invisible SVGs to prevent lazy loading -->
|
||||||
<div class="invisible h-0 w-0">
|
<div class="invisible h-0 w-0">
|
||||||
<img src="./assets/file-solid.svg">
|
<img src="./assets/file-solid.svg">
|
||||||
|
<img src="./assets/check-solid.svg">
|
||||||
<img src="./assets/circle-check-solid.svg">
|
<img src="./assets/circle-check-solid.svg">
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||||
import {DecimalPipe, NgIf} from "@angular/common";
|
import {DecimalPipe, NgClass, NgIf} from "@angular/common";
|
||||||
import {FormatFileSizePipePipe} from "../format-file-size-pipe.pipe";
|
import {FormatFileSizePipePipe} from "../format-file-size-pipe.pipe";
|
||||||
import {FormsModule} from "@angular/forms";
|
import {FormsModule} from "@angular/forms";
|
||||||
import axios, {AxiosProgressEvent} from "axios";
|
import axios, {AxiosProgressEvent} from "axios";
|
||||||
import { DevelopmentStore } from '../../store/DevelopmentStore';
|
import { DevelopmentStore } from '../../store/DevelopmentStore';
|
||||||
|
import {RouterLink} from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-upload',
|
selector: 'app-upload',
|
||||||
@ -12,7 +13,9 @@ import { DevelopmentStore } from '../../store/DevelopmentStore';
|
|||||||
NgIf,
|
NgIf,
|
||||||
FormatFileSizePipePipe,
|
FormatFileSizePipePipe,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
DecimalPipe
|
DecimalPipe,
|
||||||
|
NgClass,
|
||||||
|
RouterLink
|
||||||
],
|
],
|
||||||
templateUrl: './upload.component.html',
|
templateUrl: './upload.component.html',
|
||||||
styleUrl: './upload.component.scss'
|
styleUrl: './upload.component.scss'
|
||||||
@ -26,7 +29,6 @@ export class UploadComponent {
|
|||||||
singleDownload: boolean = false;
|
singleDownload: boolean = false;
|
||||||
fileDescription: string = '';
|
fileDescription: string = '';
|
||||||
passwordProtected: boolean = false;
|
passwordProtected: boolean = false;
|
||||||
password: string = ''; // Generated by the server for only this file
|
|
||||||
|
|
||||||
uploadStarted: boolean = false;
|
uploadStarted: boolean = false;
|
||||||
uploadProgress = 0; // Real progress
|
uploadProgress = 0; // Real progress
|
||||||
@ -34,6 +36,9 @@ export class UploadComponent {
|
|||||||
uploadFinished = false;
|
uploadFinished = false;
|
||||||
uploadSpeedBps: number = 0;
|
uploadSpeedBps: number = 0;
|
||||||
|
|
||||||
|
uploadData: FileDetails | null = null;
|
||||||
|
fileUrls: { downloadUrl: string, statusUrl: string, deleteUrl: string } | null = null;
|
||||||
|
urlCopied: boolean = false;
|
||||||
|
|
||||||
|
|
||||||
constructor(private developmentStore: DevelopmentStore) {
|
constructor(private developmentStore: DevelopmentStore) {
|
||||||
@ -97,6 +102,8 @@ export class UploadComponent {
|
|||||||
.then(response => {
|
.then(response => {
|
||||||
console.log('Upload completed successfully!');
|
console.log('Upload completed successfully!');
|
||||||
console.log(response.data);
|
console.log(response.data);
|
||||||
|
this.fileUrls = this.buildFileUrls(response.data);
|
||||||
|
this.uploadData = response.data;
|
||||||
this.uploadFinished = true;
|
this.uploadFinished = true;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -105,13 +112,26 @@ export class UploadComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFileUrls(fileDetails: FileDetails) {
|
||||||
|
const baseUrl = this.developmentStore.getBaseUrl();
|
||||||
|
const fileId = fileDetails.fileId;
|
||||||
|
const downloadUrl = `${baseUrl}download?fileId=${fileId}`;
|
||||||
|
const deleteUrl = `${baseUrl}api/v1/deletefile?fileId=${fileId}`;
|
||||||
|
const statusUrl = `${baseUrl}status?fileId=${fileId}`;
|
||||||
|
return {
|
||||||
|
downloadUrl,
|
||||||
|
statusUrl,
|
||||||
|
deleteUrl,
|
||||||
|
}; }
|
||||||
|
|
||||||
buildFormDataObject(): FormData {
|
buildFormDataObject(): FormData {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', this.fileToUpload as Blob);
|
formData.append('file', this.fileToUpload as Blob);
|
||||||
formData.append('password', this.passwordProtected ? this.password : '');
|
formData.append('passwordProtected', this.passwordProtected ? 'true' : 'false');
|
||||||
formData.append('singleDownload', this.singleDownload.toString());
|
formData.append('singleDownload', this.singleDownload.toString());
|
||||||
formData.append('shortStorage', this.shortStorage.toString());
|
formData.append('shortStorage', this.shortStorage.toString());
|
||||||
formData.append('fileDescription', this.fileDescription);
|
formData.append('fileDescription', this.fileDescription);
|
||||||
|
console.log(formData.get("passwordProtected"));
|
||||||
return formData;
|
return formData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,4 +187,24 @@ export class UploadComponent {
|
|||||||
this.uploadProgress = this.targetUploadProgress;
|
this.uploadProgress = this.targetUploadProgress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyUrlToClipboard(url: string | undefined) {
|
||||||
|
if(url) {
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
this.urlCopied = true;
|
||||||
|
console.log('Text successfully copied to clipboard');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy text to clipboard', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface FileDetails {
|
||||||
|
fileId: string;
|
||||||
|
fileName: string;
|
||||||
|
message: string;
|
||||||
|
passwordProtected: boolean;
|
||||||
|
singleDownload: boolean;
|
||||||
|
uploadDate: string;
|
||||||
|
password: string;
|
||||||
}
|
}
|
||||||
|
1
frontend/src/assets/check-solid.svg
Normal file
1
frontend/src/assets/check-solid.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#fff" d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg>
|
After Width: | Height: | Size: 424 B |
@ -29,7 +29,7 @@ public class Upload extends ApiRestController {
|
|||||||
// Currently testing
|
// Currently testing
|
||||||
@PostMapping("/upload")
|
@PostMapping("/upload")
|
||||||
public ResponseEntity<String> getUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request,
|
public ResponseEntity<String> getUpload(@RequestParam("file") MultipartFile file, HttpServletRequest request,
|
||||||
@RequestParam(value = "password", required = false) String password,
|
@RequestParam(value = "passwordProtected", required = false) Boolean passwordProtected,
|
||||||
@RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload,
|
@RequestParam(value = "singleDownload", defaultValue = "false") boolean singleDownload,
|
||||||
@RequestParam(value = "fileDescription", required = false) String fileDescription) {
|
@RequestParam(value = "fileDescription", required = false) String fileDescription) {
|
||||||
|
|
||||||
@ -40,15 +40,17 @@ public class Upload extends ApiRestController {
|
|||||||
return new ResponseEntity<>("Please select a file to upload.", HttpStatus.NOT_ACCEPTABLE);
|
return new ResponseEntity<>("Please select a file to upload.", HttpStatus.NOT_ACCEPTABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
FileUpload fileUpload = fileService.processUploadedFile(file, request.getRemoteAddr(), password, singleDownload, fileDescription);
|
FileUpload fileUpload = fileService.processUploadedFile(file, request.getRemoteAddr(), passwordProtected, singleDownload, fileDescription);
|
||||||
log.debug("User uploaded file " + file.getOriginalFilename() + " from IP " + request.getRemoteAddr() + " successfully.");
|
log.debug("User uploaded file " + file.getOriginalFilename() + " from IP " + request.getRemoteAddr() + " successfully.");
|
||||||
|
|
||||||
JsonObject response = new JsonObject();
|
JsonObject response = new JsonObject();
|
||||||
response.addProperty("fileId", fileUpload.getFileId());
|
response.addProperty("fileId", fileUpload.getFileId());
|
||||||
|
response.addProperty("fileName", fileUpload.getFileName());
|
||||||
response.addProperty("message", "File " + file.getOriginalFilename() + " uploaded successfully!");
|
response.addProperty("message", "File " + file.getOriginalFilename() + " uploaded successfully!");
|
||||||
response.addProperty("passwordProtected", fileUpload.isPasswordProtected());
|
response.addProperty("passwordProtected", fileUpload.isPasswordProtected());
|
||||||
response.addProperty("singleDownload", fileUpload.isSingleDownload());
|
response.addProperty("singleDownload", fileUpload.isSingleDownload());
|
||||||
response.addProperty("uploadDate", fileUpload.getUploadDate().toString());
|
response.addProperty("uploadDate", fileUpload.getUploadDate().toString());
|
||||||
|
response.addProperty("password", fileUpload.getDownloadPassword());
|
||||||
|
|
||||||
return new ResponseEntity<>(response.toString(), HttpStatus.OK);
|
return new ResponseEntity<>(response.toString(), HttpStatus.OK);
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,20 @@ package de.w665.sharepulse.service;
|
|||||||
import de.w665.sharepulse.exception.NoDownloadPermissionException;
|
import de.w665.sharepulse.exception.NoDownloadPermissionException;
|
||||||
import de.w665.sharepulse.model.FileUpload;
|
import de.w665.sharepulse.model.FileUpload;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import java.util.Random;
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class FileSecurityService {
|
public class FileSecurityService {
|
||||||
|
|
||||||
|
@Value("${sharepulse.filepassword-length}")
|
||||||
|
private int passwordLength;
|
||||||
|
@Value("${sharepulse.filepassword-charset}")
|
||||||
|
private String passwordCharset;
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException {
|
public boolean verifyDownloadPermission(FileUpload file, String password) throws NoDownloadPermissionException {
|
||||||
|
|
||||||
|
|
||||||
@ -34,4 +40,16 @@ public class FileSecurityService {
|
|||||||
throw new NoDownloadPermissionException("Password protected file can only be downloaded with correct password.");
|
throw new NoDownloadPermissionException("Password protected file can only be downloaded with correct password.");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String generateFilePassword() {
|
||||||
|
StringBuilder password = new StringBuilder(passwordLength);
|
||||||
|
|
||||||
|
for (int i = 0; i < passwordLength; i++) {
|
||||||
|
int randomIndex = random.nextInt(passwordCharset.length());
|
||||||
|
char randomChar = passwordCharset.charAt(randomIndex);
|
||||||
|
password.append(randomChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,15 +23,16 @@ import java.util.Date;
|
|||||||
public class FileService {
|
public class FileService {
|
||||||
|
|
||||||
private final FileIdService fileIdService;
|
private final FileIdService fileIdService;
|
||||||
|
private final FileSecurityService fileSecurityService;
|
||||||
private final FileUploadRepository fileUploadRepository;
|
private final FileUploadRepository fileUploadRepository;
|
||||||
|
|
||||||
@Value("${sharepulse.temp-filestore-path}")
|
@Value("${sharepulse.temp-filestore-path}")
|
||||||
private String tempDirPath;
|
private String tempDirPath;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FileService(FileIdService fileIdService, FileUploadRepository fileUploadRepository) {
|
public FileService(FileIdService fileIdService, FileSecurityService fileSecurityService, FileUploadRepository fileUploadRepository) {
|
||||||
this.fileIdService = fileIdService;
|
this.fileIdService = fileIdService;
|
||||||
|
this.fileSecurityService = fileSecurityService;
|
||||||
this.fileUploadRepository = fileUploadRepository;
|
this.fileUploadRepository = fileUploadRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,10 +54,15 @@ public class FileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileUpload processUploadedFile(MultipartFile file, String uploaderIp, String password, boolean singleDownload, String fileDescription) {
|
public FileUpload processUploadedFile(MultipartFile file, String uploaderIp, boolean passwordProtected, boolean singleDownload, String fileDescription) {
|
||||||
|
|
||||||
String fileId = fileIdService.generateNewUniqueId();
|
String fileId = fileIdService.generateNewUniqueId();
|
||||||
|
|
||||||
|
String password = "";
|
||||||
|
if (passwordProtected) {
|
||||||
|
password = fileSecurityService.generateFilePassword();
|
||||||
|
}
|
||||||
|
|
||||||
FileUpload fileUpload = FileUpload.builder()
|
FileUpload fileUpload = FileUpload.builder()
|
||||||
.fileId(fileId)
|
.fileId(fileId)
|
||||||
.fileName(file.getOriginalFilename())
|
.fileName(file.getOriginalFilename())
|
||||||
@ -66,7 +72,7 @@ public class FileService {
|
|||||||
.uploadedByIpAddress(uploaderIp)
|
.uploadedByIpAddress(uploaderIp)
|
||||||
.downloadCount(0)
|
.downloadCount(0)
|
||||||
.fileDescription(fileDescription)
|
.fileDescription(fileDescription)
|
||||||
.passwordProtected(password != null && !password.isEmpty())
|
.passwordProtected(passwordProtected)
|
||||||
.downloadPassword(password)
|
.downloadPassword(password)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Application config
|
# Application config
|
||||||
sharepulse.temp-filestore-path=/temp-filestore
|
sharepulse.temp-filestore-path=/temp-filestore
|
||||||
|
sharepulse.filepassword-length=6
|
||||||
|
sharepulse.filepassword-charset=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||||
|
|
||||||
# Static path
|
# Static path
|
||||||
spring.web.resources.static-locations=classpath:/static/browser/
|
spring.web.resources.static-locations=classpath:/static/browser/
|
||||||
@ -11,9 +13,6 @@ spring.data.rest.base-path=/api/v1
|
|||||||
spring.servlet.multipart.max-file-size=1GB
|
spring.servlet.multipart.max-file-size=1GB
|
||||||
spring.servlet.multipart.max-request-size=1GB
|
spring.servlet.multipart.max-request-size=1GB
|
||||||
|
|
||||||
# Logging
|
|
||||||
logging.level.de.w665.sharepulse=DEBUG
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
rethinkdb.host=localhost
|
rethinkdb.host=localhost
|
||||||
rethinkdb.port=28015
|
rethinkdb.port=28015
|
||||||
@ -25,3 +24,6 @@ spring.application.name=sharepulse
|
|||||||
|
|
||||||
# Spring profiles (Options: development, production) (Controls cors)
|
# Spring profiles (Options: development, production) (Controls cors)
|
||||||
spring.profiles.active=development
|
spring.profiles.active=development
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logging.level.de.w665.sharepulse=DEBUG
|
Loading…
x
Reference in New Issue
Block a user