diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..e6e5b60
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,24 @@
+name: Java CI with Maven
+
+on:
+ push:
+ branches: ["master", "main"]
+ pull_request:
+ branches: ["master", "main"]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 25
+ uses: actions/setup-java@v4
+ with:
+ java-version: "25"
+ distribution: "temurin"
+ cache: maven
+
+ - name: Build and Test with Maven
+ run: mvn -B clean test
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..ab0516e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,25 @@
+# Agent Instructions
+
+## GitHub CLI (`gh`) Usage
+When running `gh` commands in this project via an automated agent environment, ensure you bypass the default `GITHUB_TOKEN` environment variable. The agent environment may have an invalid `GITHUB_TOKEN` set, which `gh` prioritizes over valid keyring credentials, resulting in an `HTTP 401: Bad credentials` error.
+
+**Workaround:** Prefix `gh` commands with `env -u GITHUB_TOKEN` to force the CLI to use the valid keyring authentication.
+
+Example:
+```bash
+env -u GITHUB_TOKEN gh pr create --title "..." --body "..."
+```
+
+## Developer Guidelines & Code Architecture
+
+### 1. Spring Boot & Java Requirements
+- **Java Version**: The project is configured for **Java 17** (or newer) to align with Spring Boot 3.x requirements.
+- **Jakarta EE / Servlets**: Always use `jakarta.servlet.*` package imports instead of the legacy `javax.servlet.*` packages.
+
+### 2. Frontend Configuration & Webpack 5
+- **Node & NPM Version**: Managed by `frontend-maven-plugin` (version `1.15.0`). The project targets Node `v22.11.0` and NPM `10.9.0` to ensure build stability.
+- **Webpack Bundle**: The bundle compiles using Webpack 5 syntax and extracts styles using `mini-css-extract-plugin` rather than `extract-text-webpack-plugin`.
+- **Uppy File Uploader**:
+ - Upgraded to Uppy v5.
+ - Do **not** call `uppy.run()`. This method was deprecated/removed. Simply instantiating Uppy with `new Uppy()` and using plugins compiles and registers events automatically.
+ - CSS styles must be imported from `@uppy/core/css/style.min.css` and `@uppy/dashboard/css/style.min.css` rather than the old `dist` structure or the legacy unified `uppy` css bundle.
diff --git a/README.md b/README.md
index 047f4a2..e8af8ff 100644
--- a/README.md
+++ b/README.md
@@ -20,4 +20,5 @@ Then visit http://localhost:8080/test/ in your browser and try to upload a file
1. File `src/main/resources/public/index.html` contains the actual home page and loads the Uppy JavaScript file generated by the `uppy-file-upload` module.
* Module `uppy-file-upload` contains the Webpack and Uppy configuration to fetch and join all required JavaScript files. This is not a production ready setup!
- 1. File `uppy-file-upload/app/uppy-fileupload.js` contains the configuration parameters of the Uppy tus JS client.
\ No newline at end of file
+ 1. File `uppy-file-upload/app/uppy-fileupload.js` contains the configuration parameters of the Uppy tus JS client (upgraded to Uppy v5, configured via ES module imports and compiled using Webpack 5).
+ 2. Stylesheets are imported individually from `@uppy/core/css/style.min.css` and `@uppy/dashboard/css/style.min.css`.
\ No newline at end of file
diff --git a/spring-boot-rest/pom.xml b/spring-boot-rest/pom.xml
index fbc12ec..4a160d7 100644
--- a/spring-boot-rest/pom.xml
+++ b/spring-boot-rest/pom.xml
@@ -8,13 +8,13 @@
0.0.1-SNAPSHOT
- 1.8
+ 17
org.springframework.boot
spring-boot-starter-parent
- 1.5.18.RELEASE
+ 3.3.0
@@ -35,12 +35,17 @@
me.desair.tus
tus-java-server
- 1.0.0-2.1
+ 1.0.0-3.1
io.tus.java.client
tus-java-client
- 0.4.0
+ 0.5.0
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
diff --git a/spring-boot-rest/src/main/java/me/desair/spring/tus/App.java b/spring-boot-rest/src/main/java/me/desair/spring/tus/App.java
index 689c71e..eb1c854 100644
--- a/spring-boot-rest/src/main/java/me/desair/spring/tus/App.java
+++ b/spring-boot-rest/src/main/java/me/desair/spring/tus/App.java
@@ -40,7 +40,7 @@ public TusFileUploadService tusFileUploadService() {
return new TusFileUploadService()
.withStoragePath(tusDataPath)
.withDownloadFeature()
- .withUploadURI(servletContextPath + "/api/upload")
+ .withUploadUri(servletContextPath + "/api/upload")
.withThreadLocalCache(true);
}
diff --git a/spring-boot-rest/src/main/java/me/desair/spring/tus/FileUploadController.java b/spring-boot-rest/src/main/java/me/desair/spring/tus/FileUploadController.java
index 538b561..bbd8acb 100644
--- a/spring-boot-rest/src/main/java/me/desair/spring/tus/FileUploadController.java
+++ b/spring-boot-rest/src/main/java/me/desair/spring/tus/FileUploadController.java
@@ -2,12 +2,13 @@
import java.io.IOException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import me.desair.tus.server.TusFileUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
diff --git a/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java b/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
index 3b56809..8ba79bb 100644
--- a/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
+++ b/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
@@ -7,16 +7,14 @@
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
-public class WebMvcConfig extends WebMvcConfigurerAdapter {
+public class WebMvcConfig implements WebMvcConfigurer {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/public/" };
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
- super.addResourceHandlers(registry);
if (!registry.hasMappingForPattern("/webjars/**")) {
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
diff --git a/spring-boot-rest/src/main/resources/application.properties b/spring-boot-rest/src/main/resources/application.properties
index 6f2e63a..a87a657 100644
--- a/spring-boot-rest/src/main/resources/application.properties
+++ b/spring-boot-rest/src/main/resources/application.properties
@@ -1,4 +1,4 @@
spring.profiles.active=dev
server.port=8080
-server.context-path=/test
+server.servlet.context-path=/test
tus.server.data.directory=${java.io.tmpdir}/tus
\ No newline at end of file
diff --git a/spring-boot-rest/src/test/java/me/desair/spring/tus/FileUploadControllerTest.java b/spring-boot-rest/src/test/java/me/desair/spring/tus/FileUploadControllerTest.java
new file mode 100644
index 0000000..eee45ce
--- /dev/null
+++ b/spring-boot-rest/src/test/java/me/desair/spring/tus/FileUploadControllerTest.java
@@ -0,0 +1,45 @@
+package me.desair.spring.tus;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import me.desair.tus.server.TusFileUploadService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+public class FileUploadControllerTest {
+
+ @Autowired
+ private TusFileUploadService tusFileUploadService;
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ public void contextLoads() {
+ assertNotNull(tusFileUploadService, "TusFileUploadService should be instantiated");
+ }
+
+ @Test
+ public void testUploadEndpointOptions() throws Exception {
+ // TUS OPTIONS request should return 204 No Content
+ mockMvc.perform(options("/api/upload")
+ .header("Tus-Resumable", "1.0.0"))
+ .andExpect(status().isNoContent());
+ }
+
+ @Test
+ public void testUploadEndpointPostWithoutHeaders() throws Exception {
+ // A standard POST request without Tus headers should be rejected by the Tus service (status 412)
+ // because the required Tus-Resumable header is missing.
+ mockMvc.perform(post("/api/upload"))
+ .andExpect(status().isPreconditionFailed());
+ }
+}
diff --git a/uppy-file-upload/app/index.js b/uppy-file-upload/app/index.js
index a691044..7dd229e 100644
--- a/uppy-file-upload/app/index.js
+++ b/uppy-file-upload/app/index.js
@@ -1,2 +1,2 @@
-var fileUploader = require('./uppy-fileupload');
-fileUploader.uppy().run();
+import { uppy } from './uppy-fileupload';
+uppy();
diff --git a/uppy-file-upload/app/uppy-fileupload.js b/uppy-file-upload/app/uppy-fileupload.js
index 581aaae..db5b288 100644
--- a/uppy-file-upload/app/uppy-fileupload.js
+++ b/uppy-file-upload/app/uppy-fileupload.js
@@ -1,38 +1,31 @@
-module.exports = {
- uppy: function() {
- require("uppy/dist/uppy.min.css")
- const Uppy = require('@uppy/core')
- const Dashboard = require('@uppy/dashboard')
- const Tus = require('@uppy/tus')
+import '@uppy/core/css/style.min.css';
+import '@uppy/dashboard/css/style.min.css';
+import Uppy from '@uppy/core';
+import Dashboard from '@uppy/dashboard';
+import Tus from '@uppy/tus';
- const uppy = Uppy({
- debug: true,
- autoProceed: false,
-// restrictions: {
-// maxFileSize: 1000000,
-// maxNumberOfFiles: 3,
-// minNumberOfFiles: 2,
-// allowedFileTypes: ['image/*', 'video/*']
-// }
- })
- .use(Dashboard, {
- inline: true,
- target: '.DashboardContainer',
- replaceTargetContent: true,
- note: 'The best way to test this is with a file of several hundred MB (e.g. a Linux distro DVD image)',
- maxHeight: 450,
- metaFields: [
- { id: 'license', name: 'License', placeholder: 'specify license' },
- { id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }
- ]
- })
- .use(Tus, { endpoint: 'http://localhost:8080/test/api/upload' })
+export function uppy() {
+ const uppyInstance = new Uppy({
+ debug: true,
+ autoProceed: false,
+ })
+ .use(Dashboard, {
+ inline: true,
+ target: '.DashboardContainer',
+ replaceTargetContent: true,
+ note: 'The best way to test this is with a file of several hundred MB (e.g. a Linux distro DVD image)',
+ maxHeight: 450,
+ metaFields: [
+ { id: 'license', name: 'License', placeholder: 'specify license' },
+ { id: 'caption', name: 'Caption', placeholder: 'describe what the image is about' }
+ ]
+ })
+ .use(Tus, { endpoint: 'http://localhost:8080/test/api/upload' });
- uppy.on('upload-success', function(file, upload) {
- console.log("Upload " + file.name + " completed with URL " + upload.url);
- console.log("Developer: Now pass URL " + upload.url + " to the backend or dynmically add it to an existing form!");
- });
+ uppyInstance.on('upload-success', function(file, upload) {
+ console.log("Upload " + file.name + " completed with URL " + upload.url);
+ console.log("Developer: Now pass URL " + upload.url + " to the backend or dynamically add it to an existing form!");
+ });
- return uppy;
- }
+ return uppyInstance;
}
\ No newline at end of file
diff --git a/uppy-file-upload/package.json b/uppy-file-upload/package.json
index d828ddb..123cb32 100644
--- a/uppy-file-upload/package.json
+++ b/uppy-file-upload/package.json
@@ -4,17 +4,20 @@
"description": "",
"main": "./app",
"scripts": {
- "build": "webpack -p"
+ "build": "webpack --mode production"
},
"author": "",
"license": "ISC",
"dependencies": {
- "uppy": "1.0.0"
+ "@uppy/core": "^5.2.0",
+ "@uppy/dashboard": "^5.1.1",
+ "@uppy/tus": "^5.1.1"
},
"devDependencies": {
- "webpack": "3.10.0",
- "extract-text-webpack-plugin": "3.0.2",
- "css-loader": "0.28.8",
- "style-loader": "0.19.1"
+ "webpack": "^5.91.0",
+ "webpack-cli": "^5.1.4",
+ "mini-css-extract-plugin": "^2.9.0",
+ "css-loader": "^7.1.1",
+ "style-loader": "^4.0.0"
}
}
diff --git a/uppy-file-upload/pom.xml b/uppy-file-upload/pom.xml
index cf0bfa7..661f8d5 100644
--- a/uppy-file-upload/pom.xml
+++ b/uppy-file-upload/pom.xml
@@ -12,7 +12,7 @@
com.github.eirslett
frontend-maven-plugin
- 0.0.27
+ 1.15.0
@@ -22,8 +22,8 @@
install-node-and-npm
- v8.9.3
- 5.5.1
+ v22.11.0
+ 10.9.0
diff --git a/uppy-file-upload/webpack.config.js b/uppy-file-upload/webpack.config.js
index 1ebc4f4..457f4e2 100644
--- a/uppy-file-upload/webpack.config.js
+++ b/uppy-file-upload/webpack.config.js
@@ -1,28 +1,18 @@
-var packageJSON = require('./package.json');
-var ExtractTextPlugin = require('extract-text-webpack-plugin')
-var path = require('path');
-var webpack = require('webpack');
+const packageJSON = require('./package.json');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+const path = require('path');
const PATHS = {
build: path.join(__dirname, 'target', 'classes', 'META-INF', 'resources', 'webjars', packageJSON.name, packageJSON.version)
};
-const loaders = {
- css: {
- loader: 'css-loader'
- }
-}
-
module.exports = {
entry: './app/index.js',
- module: {
- loaders: [
+ module: {
+ rules: [
{
test: /\.css$/,
- use: ExtractTextPlugin.extract({
- fallback: 'style-loader',
- use: [loaders.css]
- })
+ use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
@@ -31,5 +21,5 @@ module.exports = {
publicPath: '/assets/',
filename: 'app-bundle.js'
},
- plugins: [new ExtractTextPlugin('[name].css')]
+ plugins: [new MiniCssExtractPlugin({ filename: '[name].css' })]
};