Skip to content

Commit

Permalink
feat: file upload with tldraw
Browse files Browse the repository at this point in the history
  • Loading branch information
Quentin-Guillemin committed Jan 23, 2024
1 parent 8ce05a1 commit 3234f4b
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 43 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"release:major": "standard-version --release-as major"
},
"dependencies": {
"@gip-recia/tldraw-webcomponent": "^1.3.0",
"@gip-recia/tldraw-webcomponent": "^1.5.1",
"@vueuse/core": "^10.7.2",
"pinia": "^2.1.7",
"vue": "^3.4.15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import fr.recia.collabsoft.configuration.beans.CorsProperties;
import fr.recia.collabsoft.configuration.beans.SecurityProperties;
import fr.recia.collabsoft.configuration.beans.SoffitProperties;
import fr.recia.collabsoft.configuration.beans.StorageProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand All @@ -40,6 +41,7 @@ public class CollabsoftProperties {
private CorsProperties cors = new CorsProperties();
private SecurityProperties security = new SecurityProperties();
private SoffitProperties soffit = new SoffitProperties();
private StorageProperties storage = new StorageProperties();

@PostConstruct
private void init() throws JsonProcessingException {
Expand All @@ -52,6 +54,7 @@ public String toString() {
+ cors + ",\n"
+ security + ",\n"
+ soffit + "\n"
+ storage
+ "\n}";
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 GIP-RECIA, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.recia.collabsoft.configuration.beans;

import lombok.Data;

@Data
public class StorageProperties {

private String location;

@Override
public String toString() {
return "\"StorageProperties\": {" +
"\n\t\"location\": \"" + location + "\"" +
"\n}";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public SoffitInterceptor(SoffitHolder soffitHolder) {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String path = request.getRequestURI().substring(request.getContextPath().length());
if (!path.startsWith("/api/file")) return true;
if (!path.startsWith("/api/file") || path.matches("^/api/file/\\d+/resource/.+$")) return true;
String token = request.getHeader("Authorization");
if (token == null) {
log.debug("No Authorization header found");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023 GIP-RECIA, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fr.recia.collabsoft.services.storage;

import com.google.common.io.Files;
import fr.recia.collabsoft.configuration.CollabsoftProperties;
import fr.recia.collabsoft.configuration.beans.StorageProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

@Slf4j
@Service
public class ResourceService {

private final StorageProperties storageProperties;

public ResourceService(CollabsoftProperties collabsoftProperties) {
this.storageProperties = collabsoftProperties.getStorage();
}

private String getFilePath(Long fileId, String resourceName) {
return storageProperties.getLocation() + File.separator + fileId + File.separator + resourceName;
}

public Resource getResource(Long fileId, String resourceName) {
return new PathResource(getFilePath(fileId, resourceName));
}

public String saveResource(Long fileId, MultipartFile file, String name) {
if (file.isEmpty()) throw new RuntimeException("File can not be empty");
try {
final String fileExt = Files.getFileExtension(file.getOriginalFilename()).toLowerCase();
String fileName = name != null && !name.trim().isEmpty() ? name : new SimpleDateFormat("yyyyMMddHHmmss'." + fileExt + "'").format(new Date());
File inFile = new File(getFilePath(fileId, fileName));
if (!inFile.getParentFile().exists()) {
boolean error = !inFile.getParentFile().mkdirs();
if (error) {
log.error("Can't create directory {} to upload file, track error!", inFile.getParentFile().getPath());
return null;
}
}

file.transferTo(inFile);

return fileName;
} catch (IOException e) {
return null;
}
}

public boolean deleteResource(Long fileId, String resourceName) {
final String path = getFilePath(fileId, resourceName);
File file = new File(path);

if (file.exists()) {
final boolean isDeleted = file.delete();
if (!isDeleted) {
log.error("Tried to delete the file {} failed, track errors!", path);
return false;
} else return true;
}
log.error("Tried to delete the file {} but it doesn't exist, track errors!", path);
return false;
}

}
69 changes: 69 additions & 0 deletions src/main/java/fr/recia/collabsoft/web/rest/FileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@
import fr.recia.collabsoft.services.db.FileHistoryService;
import fr.recia.collabsoft.services.db.FileService;
import fr.recia.collabsoft.services.db.MetadataService;
import fr.recia.collabsoft.services.storage.ResourceService;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -38,9 +42,16 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@RestController
Expand All @@ -55,6 +66,8 @@ public class FileController {
private FileHistoryService fileHistoryService;
@Autowired
private MetadataService metadataService;
@Autowired
private ResourceService resourceService;

/*
* File
Expand Down Expand Up @@ -156,6 +169,62 @@ public ResponseEntity<Object> deleteFile(@PathVariable Long id) {
return new ResponseEntity<>(HttpStatus.OK);
}

/*
* Resource
*/

/**
* Save resource
*
* @param id File id
* @param file Resource
*/
@PostMapping(value = "/{id}/resource")
public ResponseEntity<Object> postResource(@PathVariable Long id, @RequestParam("file") MultipartFile file, @Nullable @RequestParam("name") String name) {
final String fileName = resourceService.saveResource(id, file, name);
if (fileName == null) return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
Map<String, Object> data = new HashMap<>();
data.put("uri", fileName);

return new ResponseEntity<>(data, HttpStatus.CREATED);
}

/**
* Get resource
*
* @param id File id
* @param resourceName Resource name
* @return
*/
@GetMapping(value = "/{id}/resource/{resourceName:.+}")
public ResponseEntity<InputStreamResource> getResource(@PathVariable Long id, @PathVariable String resourceName) {
Resource resource = resourceService.getResource(id, resourceName);
try {
MediaType contentType = MediaType.parseMediaType(resource.getFile().toURL().openConnection().getContentType());
InputStream in = resource.getInputStream();

return ResponseEntity.ok()
.contentType(contentType)
.body(new InputStreamResource(in));
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}

/**
* Delete resource
*
* @param id File id
* @param resourceName Resource name
*/
@DeleteMapping(value = "/{id}/resource/{resourceName:.+}")
public ResponseEntity<Object> deleteResource(@PathVariable Long id, @PathVariable String resourceName) {
final boolean isResource = resourceService.deleteResource(id, resourceName);
if (!isResource) return new ResponseEntity<>(HttpStatus.NOT_FOUND);

return new ResponseEntity<>(HttpStatus.OK);
}

/*
* Metadata
*/
Expand Down
4 changes: 4 additions & 0 deletions src/main/webapp/src/views/AppView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { storeToRefs } from 'pinia';
import { onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useTheme } from 'vuetify';
const configurationStore = useConfigurationStore();
const { loadFile } = configurationStore;
Expand All @@ -20,6 +21,7 @@ const isDev = import.meta.env.DEV;
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const theme = useTheme();
if (!currentFile.value || currentFile.value.id != (route.params.fileId as unknown as number))
loadFile(parseInt(route.params.fileId as string));
Expand Down Expand Up @@ -89,7 +91,9 @@ onUnmounted(() => {
<tldraw-editor
v-if="currentFile.associatedApp.slug == AppSlug.tldraw"
:persistance-api-url="`${VITE_API_URI}/api/file/${currentFile.id}`"
:assets-api-url="`${VITE_API_URI}/api/file/${currentFile.id}/resource`"
:user-info-api-url="VITE_USER_INFO_API_URI"
:dark-mode="theme.global.name.value == 'dark'"
/>
</div>
</v-main>
Expand Down
82 changes: 41 additions & 41 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -806,14 +806,49 @@ __metadata:
languageName: node
linkType: hard

"@gip-recia/tldraw-webcomponent@npm:^1.3.0":
version: 1.3.0
resolution: "@gip-recia/tldraw-webcomponent@npm:1.3.0"
"@gip-recia/tldraw-v1@npm:^1.29.2-recia-1.0.0":
version: 1.29.2-recia-1.0.0
resolution: "@gip-recia/tldraw-v1@npm:1.29.2-recia-1.0.0"
dependencies:
"@fontsource/caveat-brush": "npm:^4.5.9"
"@fontsource/crimson-pro": "npm:^4.5.10"
"@fontsource/recursive": "npm:^4.5.11"
"@fontsource/source-code-pro": "npm:^4.5.12"
"@fontsource/source-sans-pro": "npm:^4.5.11"
"@radix-ui/react-alert-dialog": "npm:^1.0.0"
"@radix-ui/react-context-menu": "npm:^1.0.0"
"@radix-ui/react-dialog": "npm:^1.0.0"
"@radix-ui/react-dropdown-menu": "npm:^1.0.0"
"@radix-ui/react-icons": "npm:^1.1.1"
"@radix-ui/react-popover": "npm:^1.0.0"
"@radix-ui/react-tooltip": "npm:^1.0.0"
"@stitches/react": "npm:^1.2.8"
"@tldraw/core": "npm:^1.23.2"
"@tldraw/intersect": "npm:^1.9.2"
"@tldraw/vec": "npm:^1.9.2"
browser-fs-access: "npm:^0.31.0"
idb-keyval: "npm:^6.2.0"
perfect-freehand: "npm:^1.2.0"
react-error-boundary: "npm:^3.1.4"
react-hotkeys-hook: "npm:^3.4.7"
react-intl: "npm:^6.1.1"
tslib: "npm:^2.4.0"
zustand: "npm:^4.1.1"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: dd104b6e1f086d1cac9d00ac4bd0180d162ad4681f7857d040379776997425d6078c0fedda53766873df350af1cfbad2e308a0d93c17cab42853a561564f8058
languageName: node
linkType: hard

"@gip-recia/tldraw-webcomponent@npm:^1.5.1":
version: 1.5.1
resolution: "@gip-recia/tldraw-webcomponent@npm:1.5.1"
dependencies:
"@tldraw/tldraw": "npm:^1.29.2"
"@gip-recia/tldraw-v1": "npm:^1.29.2-recia-1.0.0"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
checksum: ac381bacf11d3c107c9faafd4637f34a27d2824d4c827899c971d6dc7c396f436c06cb0c9ceeb146fd4cd61a8a5da56d10f5a0f19d222c77f4fb96e0462e8801
checksum: 9d84b34ca9bd7604cc9de751711861f376f59957a31efd9f1bdfdbc31ffdc1418987360e857345a4376b71d3e8267128b3a0e0978aef40961358ba8be2797a3f
languageName: node
linkType: hard

Expand Down Expand Up @@ -2093,41 +2128,6 @@ __metadata:
languageName: node
linkType: hard

"@tldraw/tldraw@npm:^1.29.2":
version: 1.29.2
resolution: "@tldraw/tldraw@npm:1.29.2"
dependencies:
"@fontsource/caveat-brush": "npm:^4.5.9"
"@fontsource/crimson-pro": "npm:^4.5.10"
"@fontsource/recursive": "npm:^4.5.11"
"@fontsource/source-code-pro": "npm:^4.5.12"
"@fontsource/source-sans-pro": "npm:^4.5.11"
"@radix-ui/react-alert-dialog": "npm:^1.0.0"
"@radix-ui/react-context-menu": "npm:^1.0.0"
"@radix-ui/react-dialog": "npm:^1.0.0"
"@radix-ui/react-dropdown-menu": "npm:^1.0.0"
"@radix-ui/react-icons": "npm:^1.1.1"
"@radix-ui/react-popover": "npm:^1.0.0"
"@radix-ui/react-tooltip": "npm:^1.0.0"
"@stitches/react": "npm:^1.2.8"
"@tldraw/core": "npm:^1.23.2"
"@tldraw/intersect": "npm:^1.9.2"
"@tldraw/vec": "npm:^1.9.2"
browser-fs-access: "npm:^0.31.0"
idb-keyval: "npm:^6.2.0"
perfect-freehand: "npm:^1.2.0"
react-error-boundary: "npm:^3.1.4"
react-hotkeys-hook: "npm:^3.4.7"
react-intl: "npm:^6.1.1"
tslib: "npm:^2.4.0"
zustand: "npm:^4.1.1"
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
checksum: 521128651d6adda779749cbeff0c01246a27b5d779a00b596fd2b48da9b23eb5d430e41964605d377a08818525d0c5cf89fb51244aa3e94241143a5a8ae403ff
languageName: node
linkType: hard

"@tldraw/vec@npm:^1.9.2":
version: 1.9.2
resolution: "@tldraw/vec@npm:1.9.2"
Expand Down Expand Up @@ -3374,7 +3374,7 @@ __metadata:
"@fortawesome/free-regular-svg-icons": "npm:^6.5.1"
"@fortawesome/free-solid-svg-icons": "npm:^6.5.1"
"@fortawesome/vue-fontawesome": "npm:^3.0.5"
"@gip-recia/tldraw-webcomponent": "npm:^1.3.0"
"@gip-recia/tldraw-webcomponent": "npm:^1.5.1"
"@intlify/unplugin-vue-i18n": "npm:^2.0.0"
"@playwright/test": "npm:^1.41.1"
"@rushstack/eslint-patch": "npm:^1.7.0"
Expand Down

0 comments on commit 3234f4b

Please sign in to comment.