Skip to content

Commit

Permalink
Merge pull request #39 from SELab-2/feature/savefiles
Browse files Browse the repository at this point in the history
Feature/savefiles
  • Loading branch information
Matthias-VE authored Mar 7, 2024
2 parents 7bf2513 + 7131c80 commit a9af18b
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ out/

### VS Code ###
.vscode/
backend/app/data/*
2 changes: 2 additions & 0 deletions backend/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ dependencies {
implementation 'com.auth0:jwks-rsa:0.18.0'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.apache.tika:tika-core:1.27'
runtimeOnly 'org.postgresql:postgresql'


implementation "org.springframework.boot:spring-boot-devtools"
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.ugent.pidgeon.controllers;

import com.ugent.pidgeon.postgre.models.FileEntity;
import com.ugent.pidgeon.postgre.models.SubmissionEntity;
import com.ugent.pidgeon.postgre.repository.FileRepository;
import com.ugent.pidgeon.postgre.repository.GroupRepository;
import com.ugent.pidgeon.postgre.repository.ProjectRepository;
import com.ugent.pidgeon.postgre.repository.SubmissionRepository;
import com.ugent.pidgeon.util.Filehandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.nio.file.Path;
import java.sql.Timestamp;
import java.util.logging.Logger;
import java.util.zip.ZipFile;

@RestController
public class FilesubmissiontestController {

@Autowired
private GroupRepository groupRepository;
@Autowired
private FileRepository fileRepository;
@Autowired
private SubmissionRepository submissionRepository;
@Autowired
private ProjectRepository projectRepository;

@PostMapping("/project/{projectid}/submit") //Route to submit a file, it accepts a multiform with the file and submissionTime
public ResponseEntity<String> submitFile(@RequestParam("file") MultipartFile file, @RequestParam("submissionTime") Timestamp time, @PathVariable("projectid") long projectid) {
long userId = 1L; //TODO: replace with id of current user
Long groupId = groupRepository.groupIdByProjectAndUser(projectid, userId);

if (!projectRepository.userPartOfProject(projectid, userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("You aren't part of this project");
}
//TODO: executes the tests onces these are implemented
try {
//Save the file entry in the database to get the id
FileEntity fileEntity = new FileEntity("", "", userId);
long fileid = fileRepository.save(fileEntity).getId();

//Save the submission in the database TODO: update the accepted parameter
SubmissionEntity submissionEntity = new SubmissionEntity(projectid, groupId, fileid, time, false);
SubmissionEntity submission = submissionRepository.save(submissionEntity);

//Save the file on the server
Path path = Filehandler.getSubmissionPath(projectid, groupId, submission.getId());
String filename = Filehandler.saveSubmission(path, file);

//Update name and path for the file entry
fileEntity.setName(filename);
fileEntity.setPath(path.toString());
fileRepository.save(fileEntity);

return ResponseEntity.ok("File saved");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error while saving file: " + e.getMessage());
}

}

@GetMapping("submissions/{submissionid}")
public ResponseEntity<Resource> getSubmission(@PathVariable("submissionid") long submissionid) {
long userId = 1L; //TODO: replace with id of current user
// Get the submission entry from the database
SubmissionEntity submission = submissionRepository.findById(submissionid).orElse(null);
if (submission == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
if (!groupRepository.userInGroup(submission.getGroupId(), userId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(null);
}
// Get the file entry from the database
FileEntity file = fileRepository.findById(submission.getFileId()).orElse(null);
if (file == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}


// Get the file from the server
try {
Resource zipFile = Filehandler.getSubmissionAsResource(Path.of(file.getPath(), file.getName()));

// Set headers for the response
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName());
headers.add(HttpHeaders.CONTENT_TYPE, "application/zip");

return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(zipFile);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
public class GroupEntity {

@Id
@GeneratedValue
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="group_id", nullable=false)
private long id;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package com.ugent.pidgeon.postgre.models;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;

import java.sql.Timestamp;

@Entity
@Table(name="submissions")
public class SubmissionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="submission_id", nullable=false)
private long id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,28 @@ public interface GroupRepository extends JpaRepository<GroupEntity, Long>{
@Query(value= "SELECT u FROM UserEntity u JOIN GroupUserEntity gu ON u.id = gu.userId WHERE gu.groupId = ?1")
List<UserEntity> findCourseUsersByGroupId(long id);

@Query(value = """
SELECT CASE WHEN EXISTS (
SELECT gu
FROM GroupUserEntity gu
WHERE gu.userId = ?2 and gu.groupId = ?1
) THEN true ELSE false END""")
Boolean userInGroup(long groupId, long userId);


@Query(value = """
SELECT p.id FROM ProjectEntity p
JOIN GroupClusterEntity gc ON p.groupClusterId = gc.id
JOIN GroupEntity g ON g.clusterId = gc.id
WHERE g.id = ?1""")
List<Long> findProjectsByGroupId(long id);


@Query(value = """
SELECT g.id FROM GroupEntity g
JOIN GroupUserEntity gu ON g.id = gu.groupId
JOIN GroupClusterEntity gc ON g.clusterId = gc.id
JOIN ProjectEntity p ON p.groupClusterId = gc.id
WHERE p.id = ?1 AND gu.userId = ?2""")
Long groupIdByProjectAndUser(long projectId, long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@

import com.ugent.pidgeon.postgre.models.ProjectEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ProjectRepository extends JpaRepository<ProjectEntity, Long> {
List<ProjectEntity> findByCourseId(long courseId);

@Query(value = """
SELECT CASE WHEN EXISTS (
SELECT gu
FROM GroupUserEntity gu
INNER JOIN GroupEntity g ON gu.groupId = g.id
INNER JOIN GroupClusterEntity gc ON g.clusterId = gc.id
INNER JOIN ProjectEntity p ON p.groupClusterId = gc.id
WHERE gu.userId = ?1 and p.id = ?2
) THEN true ELSE false END""")
Boolean userPartOfProject(long projectId, long userId);
}
76 changes: 76 additions & 0 deletions backend/app/src/main/java/com/ugent/pidgeon/util/Filehandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.ugent.pidgeon.util;

import org.apache.tika.Tika;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.core.io.Resource;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.logging.Logger;
import java.util.zip.ZipFile;

public class Filehandler {

static String BASEPATH = "data";
static String SUBMISSION_FILENAME = "files.zip";

public static String saveSubmission(Path directory, MultipartFile file) throws IOException {
// Check if the file is empty
if (file.isEmpty()) {
throw new IOException("File is empty");
}

try {
// Create a temporary file and save the uploaded file to it
File tempFile = File.createTempFile("uploaded-zip-", ".zip");
file.transferTo(tempFile);

// Check if the file is a ZIP file
if (!isZipFile(tempFile)) {
throw new IOException("File is not a ZIP file");
}
// Create directory
File uploadDirectory = new File(directory.toString());
if (!uploadDirectory.exists()) {
if(!uploadDirectory.mkdirs()) {
throw new IOException("Error while creating directory");
}
}

// Save the file to the server
Path filePath = directory.resolve(SUBMISSION_FILENAME);

try(InputStream stream = new FileInputStream(tempFile)) {
Files.copy(stream, filePath, StandardCopyOption.REPLACE_EXISTING);
}

return filePath.getFileName().toString();
} catch (IOException e) {
throw new IOException(e.getMessage());
}
}

static public Path getSubmissionPath(long projectid, long groupid, long submissionid) {
return Path.of(BASEPATH,"projects", String.valueOf(projectid), String.valueOf(groupid), String.valueOf(submissionid));
}

public static boolean isZipFile(File file) throws IOException {
// Create a Tika instance
Tika tika = new Tika();

// Detect the file type
String fileType = tika.detect(file);

// Check if the detected file type is a ZIP file
Logger.getGlobal().info("File type: " + fileType);
return fileType.equals("application/zip") || fileType.equals("application/x-zip-compressed");

}

public static Resource getSubmissionAsResource(Path path) throws IOException {
return new InputStreamResource(new FileInputStream(path.toFile()));
}
}
4 changes: 3 additions & 1 deletion backend/app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ azure.activedirectory.tenant-id=d7811cde-ecef-496c-8f91-a1786241b99c
#spring.security.oauth2.client.registration.azure.scope=openid, profile, email
#spring.security.oauth2.client.registration.azure.provider=azure


# TODO: this is just temporary, we will need to think of an actual limit at some point
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB

#server.ssl.key-store=classpath:keystore.p12
#server.ssl.key-store-password=password
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ services:
- SPRING_DATASOURCE_USERNAME=admin
- SPRING_DATASOURCE_PASSWORD=root
restart: always
volumes:
- ./backend/app/data:/data:rw
db:
container_name: db
image: 'postgres:latest'
Expand Down

0 comments on commit a9af18b

Please sign in to comment.