From 2ceb438c2e666ff1323160ae5be2b1157da1275d Mon Sep 17 00:00:00 2001 From: Tom Desair Date: Fri, 12 Jun 2026 21:55:19 +0200 Subject: [PATCH 1/2] chore: Upgrade to Spring Boot 3.3, Webpack 5, Uppy v5, Node v22, and add JUnit tests and CI pipeline --- .github/workflows/maven.yml | 24 ++++++++ AGENTS.md | 25 ++++++++ README.md | 3 +- spring-boot-rest/pom.xml | 13 ++-- .../main/java/me/desair/spring/tus/App.java | 2 +- .../spring/tus/FileUploadController.java | 4 +- .../me/desair/spring/tus/WebMvcConfig.java | 4 +- .../src/main/resources/application.properties | 2 +- .../spring/tus/FileUploadControllerTest.java | 45 ++++++++++++++ uppy-file-upload/app/index.js | 4 +- uppy-file-upload/app/uppy-fileupload.js | 61 ++++++++----------- uppy-file-upload/package.json | 15 +++-- uppy-file-upload/pom.xml | 6 +- uppy-file-upload/webpack.config.js | 24 +++----- 14 files changed, 158 insertions(+), 74 deletions(-) create mode 100644 .github/workflows/maven.yml create mode 100644 AGENTS.md create mode 100644 spring-boot-rest/src/test/java/me/desair/spring/tus/FileUploadControllerTest.java 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 0f3c478..90b0b58 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,8 +2,8 @@ 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; 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' })] }; From 62acf4c0140c3b353a110bb3857cb81f838dfb3c Mon Sep 17 00:00:00 2001 From: Tom Desair Date: Fri, 12 Jun 2026 22:03:10 +0200 Subject: [PATCH 2/2] fix: import CrossOrigin in FileUploadController --- .../src/main/java/me/desair/spring/tus/FileUploadController.java | 1 + 1 file changed, 1 insertion(+) 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 ba92736..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 @@ -8,6 +8,7 @@ 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;