diff --git a/client/src/App.tsx b/client/src/App.tsx index a6f6381..3236f61 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -15,11 +15,12 @@ import Documents from "./pages/Documents" import Reports from "./pages/Reports" import Boilers from "./pages/Boilers" import Servers from "./pages/Servers" -import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage } from "@mui/icons-material" +import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage, Warning } from "@mui/icons-material" import Settings from "./pages/Settings" import PasswordReset from "./pages/auth/PasswordReset" import MapTest from "./pages/MapTest" import MonitorPage from "./pages/MonitorPage" +import ChunkedUpload from "./components/map/ChunkedUpload" // Определение страниц с путями и компонентом для рендера export const pages = [ @@ -127,6 +128,14 @@ export const pages = [ drawer: true, dashboard: true }, + { + label: "Chunk test", + path: "/chunk-test", + icon: , + component: , + drawer: true, + dashboard: true + }, { label: "Монитор", path: "/monitor", diff --git a/client/src/components/map/ChunkedUpload.tsx b/client/src/components/map/ChunkedUpload.tsx new file mode 100644 index 0000000..01ca68d --- /dev/null +++ b/client/src/components/map/ChunkedUpload.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import axios from 'axios'; + +const ChunkedUpload = () => { + const [file, setFile] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); + + // Handle file selection + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files) { + setFile(event.target.files[0]); + } + }; + + // Upload the file in chunks + const uploadFile = async () => { + if (!file) return; + + const chunkSize = 1024 * 1024; // 1MB per chunk + const totalChunks = Math.ceil(file.size / chunkSize); + const fileId = `${file.name}-${Date.now()}`; // Unique file identifier + let uploadedChunks = 0; + + for (let start = 0; start < file.size; start += chunkSize) { + const chunk = file.slice(start, start + chunkSize); + const chunkNumber = Math.ceil(start / chunkSize) + 1; + + try { + await axios.post(`${import.meta.env.VITE_API_EMS_URL}/upload`, chunk, { + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Chunk-Number': chunkNumber.toString(), + 'X-Total-Chunks': totalChunks.toString(), + 'X-File-Id': fileId, + }, + }); + uploadedChunks++; + setUploadProgress((uploadedChunks / totalChunks) * 100); + } catch (error) { + console.error('Chunk upload failed', error); + // Implement retry logic if needed + break; + } + } + }; + + return ( +
+ + +
Upload Progress: {uploadProgress.toFixed(2)}%
+
+ ); +}; + +export default ChunkedUpload; \ No newline at end of file diff --git a/client/src/components/map/MapComponent.tsx b/client/src/components/map/MapComponent.tsx index 56b56ec..e13f007 100644 --- a/client/src/components/map/MapComponent.tsx +++ b/client/src/components/map/MapComponent.tsx @@ -4,29 +4,35 @@ import 'ol/ol.css' import Map from 'ol/Map' import View from 'ol/View' import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction' -import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source' +import { ImageStatic, OSM, TileDebug, Vector as VectorSource, XYZ } from 'ol/source' import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer' -import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material' +import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography } from '@mui/material' import { Add, Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Warning } from '@mui/icons-material' import { Type } from 'ol/geom/Geometry' import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import Feature from 'ol/Feature' import { SatelliteMapsProvider } from '../../interfaces/map' -import { boundingExtent, containsExtent, Extent, getBottomLeft, getBottomRight, getCenter, getHeight, getTopLeft, getTopRight, getWidth } from 'ol/extent' +import { containsExtent, Extent, getCenter, getHeight, getWidth } from 'ol/extent' import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles' import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources' -import { mapCenter } from './MapConstants' +import { mapCenter, mapExtent } from './MapConstants' import ImageLayer from 'ol/layer/Image' import VectorImageLayer from 'ol/layer/VectorImage' import { LineString, MultiPoint, Point, Polygon, SimpleGeometry } from 'ol/geom' import { fromExtent } from 'ol/geom/Polygon' import Collection from 'ol/Collection' -import { Coordinate, distance, rotate } from 'ol/coordinate' +import { Coordinate } from 'ol/coordinate' import { Stroke, Fill, Circle as CircleStyle, Style } from 'ol/style' -import { addCoordinateTransforms, addProjection, get, getTransform, Projection, transform } from 'ol/proj' -import proj4 from 'proj4' +import { calculateExtent, calculateRotationAngle, rotateProjection } from './mapUtils' +import MapBrowserEvent from 'ol/MapBrowserEvent' +import { get } from 'ol/proj' const MapComponent = () => { + const [currentCoordinate, setCurrentCoordinate] = useState(null) + const [currentZ, setCurrentZ] = useState(undefined) + const [currentX, setCurrentX] = useState(undefined) + const [currentY, setCurrentY] = useState(undefined) + const mapElement = useRef(null) const [currentTool, setCurrentTool] = useState(null) @@ -191,88 +197,6 @@ const MapComponent = () => { }; } - function rotateProjection(projection, angle, extent) { - function rotateCoordinate(coordinate, angle, anchor) { - var coord = rotate( - [coordinate[0] - anchor[0], coordinate[1] - anchor[1]], - angle - ); - return [coord[0] + anchor[0], coord[1] + anchor[1]]; - } - - function rotateTransform(coordinate: Coordinate) { - return rotateCoordinate(coordinate, angle, getCenter(extent)); - } - - function normalTransform(coordinate: Coordinate) { - return rotateCoordinate(coordinate, -angle, getCenter(extent)); - } - - var normalProjection = get(projection); - - var rotatedProjection = new Projection({ - code: - normalProjection.getCode() + - ":" + - angle.toString() + - ":" + - extent.toString(), - units: normalProjection.getUnits(), - extent: extent - }); - addProjection(rotatedProjection); - - addCoordinateTransforms( - "EPSG:4326", - rotatedProjection, - function (coordinate) { - return rotateTransform(transform(coordinate, "EPSG:4326", projection)); - }, - function (coordinate) { - return transform(normalTransform(coordinate), projection, "EPSG:4326"); - } - ); - - addCoordinateTransforms( - "EPSG:3857", - rotatedProjection, - function (coordinate) { - return rotateTransform(transform(coordinate, "EPSG:3857", projection)); - }, - function (coordinate) { - return transform(normalTransform(coordinate), projection, "EPSG:3857"); - } - ); - - // also set up transforms with any projections defined using proj4 - if (typeof proj4 !== "undefined") { - var projCodes = Object.keys(proj4.defs); - projCodes.forEach(function (code) { - var proj4Projection = get(code); - if (!getTransform(proj4Projection, rotatedProjection)) { - addCoordinateTransforms( - proj4Projection, - rotatedProjection, - function (coordinate) { - return rotateTransform( - transform(coordinate, proj4Projection, projection) - ); - }, - function (coordinate) { - return transform( - normalTransform(coordinate), - projection, - proj4Projection - ); - } - ); - } - }); - } - - return rotatedProjection; - } - const handleImageDrop = useCallback((event: any) => { event.preventDefault(); event.stopPropagation(); @@ -383,41 +307,6 @@ const MapComponent = () => { } }); - const calculateCentroid = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => { - const x = (bottomLeft[0] + topLeft[0] + topRight[0] + bottomRight[0]) / 4; - const y = (bottomLeft[1] + topLeft[1] + topRight[1] + bottomRight[1]) / 4; - return [x, y]; - } - - const calculateRotationAngle = (bottomLeft: Coordinate, bottomRight: Coordinate) => { - // Calculate the difference in x and y coordinates between bottom right and bottom left - const deltaX = bottomRight[0] - bottomLeft[0]; - const deltaY = bottomRight[1] - bottomLeft[1]; - - // Calculate the angle using atan2 - const angle = Math.atan2(deltaY, deltaX); - - return angle; - } - - const calculateExtent = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => { - const width = distance(bottomLeft, bottomRight); - const height = distance(bottomLeft, topLeft); - - // Calculate the centroid of the polygon - const [centerX, centerY] = calculateCentroid(bottomLeft, topLeft, topRight, bottomRight); - - // Define the extent based on the center and dimensions - const extent = [ - centerX - width / 2, // minX - centerY - height / 2, // minY - centerX + width / 2, // maxX - centerY + height / 2 // maxY - ]; - - return extent; - } - // Function to update the image layer with a new source when extent changes const updateImageSource = () => { const newExtent = polygonFeature.getGeometry()?.getExtent(); @@ -430,10 +319,34 @@ const MapComponent = () => { if (newExtent && bottomLeft && bottomRight && topRight && topLeft) { const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight) + const worldExtent = get('EPSG:3857')?.getExtent() as Extent + const zoomLevel = Number(map.current?.getView().getZoom()?.toFixed(0)) + const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft[0], bottomLeft[1], worldExtent, zoomLevel) + const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft[0], topLeft[1], worldExtent, zoomLevel) + const { tileX: trX, tileY: trY } = getGridCellPosition(topRight[0], topRight[1], worldExtent, zoomLevel) + const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight[0], topRight[1], worldExtent, zoomLevel) + const minX = Math.min(blX, tlX, trX, brX) + const maxX = Math.max(blX, tlX, trX, brX) + const minY = Math.min(blY, tlY, trY, brY) + const maxY = Math.max(blY, tlY, trY, brY) + + const mapWidth = Math.abs(worldExtent[0] - worldExtent[2]) + const mapHeight = Math.abs(worldExtent[1] - worldExtent[3]) + const tileWidth = mapWidth / (zoomLevel * 4) + const tileHeight = mapHeight / (zoomLevel * 4) + const newMinX = worldExtent[0] + (tileWidth * minX) + const newMaxX = worldExtent[0] + (tileWidth * (maxX + 1)) + + const newMinY = worldExtent[1] + (tileHeight * (maxY + 1)) + const newMaxY = worldExtent[1] + (tileHeight * minY) + + console.log('Tile slippy bounds: ', minX, maxX, minY, maxY) + console.log('Tile bounds: ', newMinX, newMaxX, newMinY, newMaxY) + const newImageSource = new ImageStatic({ url: imageUrl, imageExtent: originalExtent, - projection: rotateProjection('EPSG:3857', -calculateRotationAngle(bottomLeft, bottomRight), originalExtent) + projection: rotateProjection('EPSG:3857', calculateRotationAngle(bottomLeft, bottomRight), originalExtent) }); imageLayer.current.setSource(newImageSource); } @@ -535,6 +448,36 @@ const MapComponent = () => { }) } + function getTilesPerSide(zoom: number) { + return Math.pow(2, zoom) + } + + function normalize(value: number, min: number, max: number) { + return (value - min) / (max - min) + } + + function getTileIndex(normalized: number, tilesPerSide: number) { + return Math.floor(normalized * tilesPerSide) + } + + function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) { + const tilesPerSide = getTilesPerSide(zoom); + const minX = extent[0] + const minY = extent[1] + const maxX = extent[2] + const maxY = extent[3] + + // Normalize the coordinates + const xNormalized = normalize(x, minX, maxX); + const yNormalized = normalize(y, minY, maxY); + + // Get tile indices + const tileX = getTileIndex(xNormalized, tilesPerSide); + const tileY = getTileIndex(1 - yNormalized, tilesPerSide); + + return { tileX, tileY }; + } + useEffect(() => { drawingLayer.current = new VectorLayer({ source: drawingLayerSource.current, @@ -589,7 +532,9 @@ const MapComponent = () => { }) map.current = new Map({ - layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current], + layers: [baseLayer.current, new TileLayer({ + source: new TileDebug(), + }), satLayer.current, regionsLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current], target: mapElement.current as HTMLDivElement, view: new View({ center: mapCenter, @@ -599,6 +544,16 @@ const MapComponent = () => { }), }) + map.current.on('pointermove', function (e: MapBrowserEvent) { + setCurrentCoordinate(e.coordinate) + const currentExtent = get('EPSG:3857')?.getExtent() as Extent + + const { tileX, tileY } = getGridCellPosition(e.coordinate[0], e.coordinate[1], currentExtent, Number(map.current?.getView().getZoom()?.toFixed(0))) + setCurrentZ(Number(map.current?.getView().getZoom()?.toFixed(0))) + setCurrentX(tileX) + setCurrentY(tileY) + }) + const modify = new Modify({ source: drawingLayerSource.current }) map.current.addInteraction(modify) @@ -678,6 +633,16 @@ const MapComponent = () => { + + + {currentCoordinate?.[0]}-{currentCoordinate?.[1]} + + + + Z={currentZ} + X={currentX} + Y={currentY} + }> diff --git a/client/src/components/map/mapUtils.ts b/client/src/components/map/mapUtils.ts new file mode 100644 index 0000000..402ee8c --- /dev/null +++ b/client/src/components/map/mapUtils.ts @@ -0,0 +1,127 @@ +import { Coordinate, distance, rotate } from "ol/coordinate"; +import { Extent, getCenter } from "ol/extent"; +import { addCoordinateTransforms, addProjection, get, getTransform, Projection, ProjectionLike, transform } from "ol/proj"; +import proj4 from "proj4"; + +function rotateProjection(projection: ProjectionLike, angle: number, extent: Extent) { + function rotateCoordinate(coordinate: Coordinate, angle: number, anchor: Coordinate) { + var coord = rotate( + [coordinate[0] - anchor[0], coordinate[1] - anchor[1]], + angle + ); + return [coord[0] + anchor[0], coord[1] + anchor[1]]; + } + + function rotateTransform(coordinate: Coordinate) { + return rotateCoordinate(coordinate, angle, getCenter(extent)); + } + + function normalTransform(coordinate: Coordinate) { + return rotateCoordinate(coordinate, -angle, getCenter(extent)); + } + + var normalProjection = get(projection); + + if (normalProjection) { + var rotatedProjection = new Projection({ + code: normalProjection.getCode() + ":" + angle.toString() + ":" + extent.toString(), + units: normalProjection.getUnits(), + extent: extent + }); + addProjection(rotatedProjection); + + addCoordinateTransforms( + "EPSG:4326", + rotatedProjection, + function (coordinate) { + return rotateTransform(transform(coordinate, "EPSG:4326", projection)); + }, + function (coordinate) { + return transform(normalTransform(coordinate), projection, "EPSG:4326"); + } + ); + + addCoordinateTransforms( + "EPSG:3857", + rotatedProjection, + function (coordinate) { + return rotateTransform(transform(coordinate, "EPSG:3857", projection)); + }, + function (coordinate) { + return transform(normalTransform(coordinate), projection, "EPSG:3857"); + } + ); + + // also set up transforms with any projections defined using proj4 + if (typeof proj4 !== "undefined") { + var projCodes = Object.keys(proj4.defs); + projCodes.forEach(function (code) { + var proj4Projection = get(code) as Projection; + if (proj4Projection) { + if (!getTransform(proj4Projection, rotatedProjection)) { + addCoordinateTransforms( + proj4Projection, + rotatedProjection, + function (coordinate) { + return rotateTransform( + transform(coordinate, proj4Projection, projection) + ); + }, + function (coordinate) { + return transform( + normalTransform(coordinate), + projection, + proj4Projection + ); + } + ); + } + } + }); + } + + return rotatedProjection; + } +} + +const calculateCentroid = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => { + const x = (bottomLeft[0] + topLeft[0] + topRight[0] + bottomRight[0]) / 4; + const y = (bottomLeft[1] + topLeft[1] + topRight[1] + bottomRight[1]) / 4; + return [x, y]; +} + +function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) { + // Calculate the difference in x and y coordinates between bottom right and bottom left + const deltaX = bottomRight[0] - bottomLeft[0]; + const deltaY = bottomRight[1] - bottomLeft[1]; + + // Calculate the angle using atan2 + const angle = -Math.atan2(deltaY, deltaX); + + return angle; +} + +function calculateExtent(bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) { + const width = distance(bottomLeft, bottomRight); + const height = distance(bottomLeft, topLeft); + + // Calculate the centroid of the polygon + const [centerX, centerY] = calculateCentroid(bottomLeft, topLeft, topRight, bottomRight); + + // Define the extent based on the center and dimensions + const extent = [ + centerX - width / 2, // minX + centerY - height / 2, // minY + centerX + width / 2, // maxX + centerY + height / 2 // maxY + ]; + + return extent; +} + +export { + rotateProjection, + calculateRotationAngle, + calculateExtent, + calculateCentroid +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9ecfa8e..cb69d90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,7 +28,7 @@ services: context: ./ems dockerfile: Dockerfile volumes: - - ./ems/tile_data:/app/tiles + - ./ems/public:/app/public links: - redis_db:redis_db - psql_db:psql_db diff --git a/ems/.gitignore b/ems/.gitignore index b80f573..a9cb726 100644 --- a/ems/.gitignore +++ b/ems/.gitignore @@ -25,3 +25,4 @@ dist-ssr # Docker volumes tile_data +public diff --git a/ems/package-lock.json b/ems/package-lock.json index 00efd44..4c30e96 100644 --- a/ems/package-lock.json +++ b/ems/package-lock.json @@ -16,11 +16,20 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "ioredis": "^5.4.1", - "prisma": "^5.18.0" + "md5": "^2.3.0", + "multer": "^1.4.5-lts.1", + "prisma": "^5.18.0", + "pump": "^3.0.0", + "sharp": "^0.33.5" }, "devDependencies": { + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/md5": "^2.3.5", + "@types/multer": "^1.4.12", "@types/node": "^22.4.1", + "@types/pump": "^1.1.3", "@types/redis": "^4.0.11", "nodemon": "^3.1.4", "ts-node": "^10.9.2", @@ -39,6 +48,357 @@ "node": ">=12" } }, + "node_modules/@emnapi/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -228,6 +588,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -258,12 +627,27 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/md5": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.4.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.1.tgz", @@ -273,6 +657,15 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/pump": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/pump/-/pump-1.1.3.tgz", + "integrity": "sha512-ZyooTTivmOwPfOwLVaszkF8Zq6mvavgjuHYitZhrIjfQAJDH+kIP3N+MzpG1zDAslsHvVz6Q8ECfivix3qLJaQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -365,6 +758,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -454,6 +852,22 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -480,6 +894,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -512,6 +934,43 @@ "node": ">=0.10.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -529,6 +988,20 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -561,6 +1034,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -579,6 +1057,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -636,6 +1122,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -669,6 +1163,14 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -1033,6 +1535,11 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1045,6 +1552,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1075,6 +1587,11 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -1091,6 +1608,16 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1154,11 +1681,47 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -1257,6 +1820,14 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1297,6 +1868,11 @@ "node": ">=16.13" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1320,6 +1896,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1356,6 +1941,25 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1432,7 +2036,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -1503,6 +2106,44 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -1520,6 +2161,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1545,6 +2194,27 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1629,6 +2299,12 @@ } } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "optional": true + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1641,6 +2317,11 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -1674,6 +2355,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1696,6 +2382,19 @@ "node": ">= 0.8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/ems/package.json b/ems/package.json index 57a3562..887cc90 100644 --- a/ems/package.json +++ b/ems/package.json @@ -20,11 +20,20 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "ioredis": "^5.4.1", - "prisma": "^5.18.0" + "md5": "^2.3.0", + "multer": "^1.4.5-lts.1", + "prisma": "^5.18.0", + "pump": "^3.0.0", + "sharp": "^0.33.5" }, "devDependencies": { + "@types/body-parser": "^1.19.5", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/md5": "^2.3.5", + "@types/multer": "^1.4.12", "@types/node": "^22.4.1", + "@types/pump": "^1.1.3", "@types/redis": "^4.0.11", "nodemon": "^3.1.4", "ts-node": "^10.9.2", diff --git a/ems/src/index.ts b/ems/src/index.ts index 1f49817..33a17d8 100644 --- a/ems/src/index.ts +++ b/ems/src/index.ts @@ -2,11 +2,107 @@ import express, { Request, Response } from 'express'; import fs from 'fs'; import path from 'path'; import axios from 'axios'; +import multer from 'multer' +import sharp from 'sharp'; +import { pipeline } from 'stream'; +import pump from 'pump' +import md5 from 'md5' +import bodyParser from 'body-parser'; +import cors from 'cors' const app = express(); -const PORT = process.env.EMS_PORT; +const PORT = process.env.EMS_PORT || 5000; -const tileFolder = path.join(__dirname, '..', 'tiles'); +const tileFolder = path.join(__dirname, '..', 'public', 'tile_data'); +const uploadDir = path.join(__dirname, '..', 'public', 'uploads'); + +interface UploadProgress { + receivedChunks: Set; + totalChunks: number; +} + +const uploadProgress: Record = {}; + +app.use(bodyParser.raw({ + type: 'application/octet-stream', + limit: '100mb' +})) + +app.use(cors()) + +// Upload chunk handler +app.post('/upload', (req: Request, res: Response) => { + const chunkNumber = parseInt(req.headers['x-chunk-number'] as string, 10); + const totalChunks = parseInt(req.headers['x-total-chunks'] as string, 10); + const fileId = req.headers['x-file-id'] as string; + + if (isNaN(chunkNumber) || isNaN(totalChunks) || !fileId) { + return res.status(400).send('Invalid headers'); + } + + const chunkDir = path.join(uploadDir, fileId); + if (!fs.existsSync(chunkDir)) { + fs.mkdirSync(chunkDir, { recursive: true }); + } + + // Save the chunk + const chunkPath = path.join(chunkDir, `chunk-${chunkNumber}`); + fs.writeFileSync(chunkPath, req.body); + + // Initialize or update upload progress + if (!uploadProgress[fileId]) { + uploadProgress[fileId] = { receivedChunks: new Set(), totalChunks }; + } + uploadProgress[fileId].receivedChunks.add(chunkNumber); + + // Check if all chunks have been received + if (uploadProgress[fileId].receivedChunks.size === totalChunks) { + assembleChunks(fileId, chunkDir) + .then(() => { + delete uploadProgress[fileId]; // Clean up progress tracking + res.status(200).send('File assembled successfully'); + }) + .catch((error) => { + console.error('Error assembling file:', error); + res.status(500).send('Failed to assemble file'); + }); + } else { + res.status(200).send('Chunk received'); + } +}); + +// Assemble chunks into final file +async function assembleChunks(fileId: string, chunkDir: string): Promise { + const finalPath = path.join(uploadDir, fileId); + const chunks = fs.readdirSync(chunkDir).sort((a, b) => { + const numA = parseInt(a.split('-')[1]); + const numB = parseInt(b.split('-')[1]); + return numA - numB; + }); + + const writeStream = fs.createWriteStream(finalPath); + for (const chunk of chunks) { + const chunkPath = path.join(chunkDir, chunk); + const data = fs.readFileSync(chunkPath); + writeStream.write(data); + fs.unlinkSync(chunkPath); // Remove chunk after writing + } + + writeStream.end(() => { + fs.rmdirSync(chunkDir); // Remove temporary chunk directory + }); +} + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, path.join(__dirname, '..', 'public', 'uploads')) + }, + filename: function (req, file, cb) { + cb(null, file.originalname) + } +}) + +const upload = multer({ storage: storage }) const fetchTileFromAPI = async (provider: string, z: string, x: string, y: string): Promise => { const url = provider === 'google' @@ -15,7 +111,7 @@ const fetchTileFromAPI = async (provider: string, z: string, x: string, y: strin const response = await axios.get(url, { responseType: 'arraybuffer' }); return response.data; -}; +} app.get('/tile/:provider/:z/:x/:y', async (req: Request, res: Response) => { const { provider, z, x, y } = req.params;