mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
Merge branch 'main' into restore/pr-6224
This commit is contained in:
commit
deb77f0150
17 changed files with 5144 additions and 216 deletions
5
.changeset/empty-chefs-arrive.md
Normal file
5
.changeset/empty-chefs-arrive.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/locales": patch
|
||||
---
|
||||
|
||||
@uppy/locales: update Norwegian Bokmål (nb_NO)
|
||||
5
.changeset/little-clouds-call.md
Normal file
5
.changeset/little-clouds-call.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/tus": major
|
||||
---
|
||||
|
||||
@uppy/tus: don't abort the request on error, so the server response (status + body) is forwarded to the `upload-error` event and `file.response` instead of being reset to status `0`.
|
||||
5
.changeset/purple-vans-act.md
Normal file
5
.changeset/purple-vans-act.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/companion": patch
|
||||
---
|
||||
|
||||
Reduce companion redis key timeouts to 10 min - In theory this shouldn't affect users
|
||||
5
.changeset/quiet-hounds-rest.md
Normal file
5
.changeset/quiet-hounds-rest.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/angular": minor
|
||||
---
|
||||
|
||||
Add support for Angular 21
|
||||
|
|
@ -9,22 +9,22 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.17",
|
||||
"@angular/common": "^19.2.17",
|
||||
"@angular/compiler": "^19.2.18",
|
||||
"@angular/core": "^19.2.18",
|
||||
"@angular/forms": "^19.2.17",
|
||||
"@angular/platform-browser": "^19.2.17",
|
||||
"@angular/platform-browser-dynamic": "^19.2.17",
|
||||
"@angular/router": "^19.2.17",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"@angular/animations": "^21.2.10",
|
||||
"@angular/common": "^21.2.10",
|
||||
"@angular/compiler": "^21.2.10",
|
||||
"@angular/core": "^21.2.10",
|
||||
"@angular/forms": "^21.2.10",
|
||||
"@angular/platform-browser": "^21.2.10",
|
||||
"@angular/platform-browser-dynamic": "^21.2.10",
|
||||
"@angular/router": "^21.2.10",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.0",
|
||||
"@angular/cli": "^19.0.0",
|
||||
"@angular/compiler-cli": "^19.2.17",
|
||||
"@angular-devkit/build-angular": "^21.2.8",
|
||||
"@angular/cli": "^21.2.8",
|
||||
"@angular/compiler-cli": "^21.2.10",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"ng-packagr": "^19.0.0",
|
||||
"typescript": "~5.5.4"
|
||||
"ng-packagr": "^21.2.3",
|
||||
"typescript": "~5.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@
|
|||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
||||
"@angular/core": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
||||
"@uppy/core": "workspace:^",
|
||||
"@uppy/dashboard": "workspace:^",
|
||||
"@uppy/status-bar": "workspace:^"
|
||||
|
|
|
|||
|
|
@ -82,14 +82,14 @@
|
|||
"@types/compression": "1.8.1",
|
||||
"@types/content-disposition": "0.5.9",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.6",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/eslint": "9.6.1",
|
||||
"@types/express-session": "1.19.0",
|
||||
"@types/http-proxy": "1.17.17",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/lodash": "4.17.24",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/morgan": "1.7.37",
|
||||
"@types/morgan": "1.9.10",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "20.19.41",
|
||||
"@types/node-schedule": "2.1.8",
|
||||
|
|
|
|||
|
|
@ -602,8 +602,11 @@ export default class Uploader {
|
|||
saveState(state: { action: string; payload: unknown }): void {
|
||||
if (!this.storage) return
|
||||
// make sure the keys get cleaned up.
|
||||
// We don't need more than 10 minutes because progress events should make sure that it doesn't expire before the upload is done.
|
||||
// assume that after the upload is done, we don't need it around for long
|
||||
// https://github.com/transloadit/uppy/issues/3748
|
||||
const keyExpirySec = 60 * 60 * 24
|
||||
// https://github.com/transloadit/api2/pull/8345#pullrequestreview-4552806932
|
||||
const keyExpirySec = 60 * 10
|
||||
const redisKey = `${Uploader.STORAGE_PREFIX}:${this.token}`
|
||||
this.storage.set(redisKey, jsonStringify(state), 'EX', keyExpirySec)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,14 @@ export default function connect(
|
|||
next(err)
|
||||
return
|
||||
}
|
||||
stateObj.origin = finalOrigin
|
||||
// RegExp is not allowed in the state object because it cannot be serialized to JSON, so we filter it out here.
|
||||
if (Array.isArray(finalOrigin)) {
|
||||
stateObj.origin = finalOrigin.flatMap((v) =>
|
||||
v instanceof RegExp ? [] : [v],
|
||||
)
|
||||
} else if (!(finalOrigin instanceof RegExp)) {
|
||||
stateObj.origin = finalOrigin
|
||||
}
|
||||
encodeStateAndRedirect(req, res, stateObj)
|
||||
})
|
||||
return
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ function getCommonCookieOptions({
|
|||
return cookieOptions
|
||||
}
|
||||
|
||||
const getCookieName = (oauthProvider: string): string =>
|
||||
export const getCookieName = (oauthProvider: string): string =>
|
||||
`uppyAuthToken--${oauthProvider}`
|
||||
|
||||
const addToCookies = ({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { decrypt, encrypt } from './utils.js'
|
|||
|
||||
export type OAuthState = {
|
||||
id: string
|
||||
origin?: string | string[] | number | boolean | undefined // weird type because this is what express's res.getHeader and cors callback combined can possibly return
|
||||
origin?: string | number | boolean | (string | boolean)[] | undefined // weird type because this is what express's res.getHeader and cors callback combined can possibly return
|
||||
preAuthToken?: string
|
||||
companionInstance?: string
|
||||
customerDefinedAllowedOrigins?: string[]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import packageJson from '../../package.json' with { type: 'json' }
|
|||
import type { CompanionRuntimeOptions } from '../types/companion-options.js'
|
||||
import type { ProviderUserSession } from '../types/express.js'
|
||||
import * as tokenService from './helpers/jwt.js'
|
||||
import { getCookieName } from './helpers/jwt.js'
|
||||
import { getURLBuilder } from './helpers/utils.js'
|
||||
import * as logger from './logger.js'
|
||||
import { isOAuthProvider } from './provider/Provider.js'
|
||||
|
|
@ -175,7 +176,7 @@ export const cookieAuthToken: RequestHandler = (req, res, next) => {
|
|||
if (oauthProvider == null || oauthProvider.length === 0) {
|
||||
return next()
|
||||
}
|
||||
req.companion.authToken = req.cookies[`uppyAuthToken--${oauthProvider}`]
|
||||
req.companion.authToken = req.cookies[getCookieName(oauthProvider)]
|
||||
return next()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export default function server(inputCompanionOptions?: CompanionInitOptions) {
|
|||
})
|
||||
// log server requests.
|
||||
router.use(morgan('combined'))
|
||||
morgan.token('url', (req) => {
|
||||
morgan.token<Request>('url', (req) => {
|
||||
const { query, censored } = censorQuery(req.query)
|
||||
return censored
|
||||
? `${req.path}?${qs.stringify(query)}`
|
||||
|
|
@ -136,13 +136,21 @@ export default function server(inputCompanionOptions?: CompanionInitOptions) {
|
|||
})
|
||||
}
|
||||
|
||||
if (process.env['COMPANION_COOKIE_DOMAIN']) {
|
||||
sessionOptions.cookie = {
|
||||
domain: process.env['COMPANION_COOKIE_DOMAIN'],
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||
}
|
||||
const cookieOptions: SessionOptions['cookie'] = {
|
||||
// AFAIK sessions are only used for the initial oauth2 dance, and not after that.
|
||||
// Therefore 10 minutes should be plenty of time for the user to complete the oauth2 dance.
|
||||
// we might want to see if we can make grant work without sessions, so we can remove sessions:
|
||||
// https://github.com/transloadit/uppy/issues/4394
|
||||
// https://github.com/transloadit/api2/pull/8345#pullrequestreview-4552806932
|
||||
maxAge: 10 * 60 * 1000,
|
||||
}
|
||||
|
||||
if (process.env['COMPANION_COOKIE_DOMAIN']) {
|
||||
cookieOptions.domain = process.env['COMPANION_COOKIE_DOMAIN']
|
||||
}
|
||||
|
||||
sessionOptions.cookie = cookieOptions
|
||||
|
||||
// Session is used for grant redirects, so that we don't need to expose secret tokens in URLs
|
||||
// See https://github.com/transloadit/uppy/pull/1668
|
||||
// https://github.com/transloadit/uppy/issues/3538#issuecomment-1069232909
|
||||
|
|
|
|||
|
|
@ -13,22 +13,31 @@ const nb_NO: Locale<0 | 1> = {
|
|||
nb_NO.strings = {
|
||||
addBulkFilesFailed: {
|
||||
'0': 'Kunne ikke legge til %{smart_count} fil på grunn av en intern feil',
|
||||
'1': 'Kunne ikke legge til %{smart_count} filer på grunn av en intern feil',
|
||||
'1': 'Kunne ikke legge til %{smart_count} filer på grunn av interne feil',
|
||||
},
|
||||
addedNumFiles: 'La til %{numFiles} fil(er)',
|
||||
addingMoreFiles: 'Legger til flere filer',
|
||||
additionalRestrictionsFailed:
|
||||
'%{count} ekstra restriksjoner ble ikke oppfylt',
|
||||
addMore: 'Legg til',
|
||||
addMoreFiles: 'Legg til filer',
|
||||
addingMoreFiles: 'Legger til flere filer',
|
||||
aggregateExceedsSize:
|
||||
'Du valgte %{size} med filer, men maksimal tillatt størrelse er %{sizeAllowed}',
|
||||
allFilesFromFolderNamed: 'Alle filer fra mappen %{name}',
|
||||
allowAccessDescription:
|
||||
'For å kunne ta bilder eller spille inn video må du gi siden tilgang til å bruke ditt kamera.',
|
||||
allowAccessTitle: 'Vennligst gi tilgang til ditt kamera',
|
||||
authAborted: 'Autentisering avbrutt',
|
||||
allowAudioAccessDescription:
|
||||
'For å kunne ta opp lyd må du gi siden tilgang til mikrofonen.',
|
||||
allowAudioAccessTitle: 'Vennligst gi tilgang til mikrofonen din',
|
||||
aspectRatioLandscape: 'Beskjær landskap (16:9)',
|
||||
aspectRatioPortrait: 'Beskjær portrett (9:16)',
|
||||
aspectRatioSquare: 'Beskjær kvadrat',
|
||||
authAborted: 'Autentisering avbrutt',
|
||||
authenticate: 'Koble til',
|
||||
authenticateWith: 'Koble til %{pluginName}',
|
||||
authenticateWithTitle: 'Koble til %{pluginName} for å velge filer',
|
||||
back: 'Tilbake',
|
||||
backToSearch: 'Tilbake til Søk',
|
||||
browse: 'velg',
|
||||
browseFiles: 'velg filer',
|
||||
browseFolders: 'velg mapper',
|
||||
|
|
@ -39,6 +48,8 @@ nb_NO.strings = {
|
|||
companionUnauthorizeHint:
|
||||
'For å logge ut av din %{provider}-konto, gå til %{url}',
|
||||
complete: 'Fullført',
|
||||
compressedX: 'Sparte %{size} ved å komprimere bilder',
|
||||
compressingImages: 'Komprimerer bilder...',
|
||||
connectedToInternet: 'Koblet til internett',
|
||||
copyLink: 'Kopier lenke',
|
||||
copyLinkToClipboardFallback: 'Kopier URL under',
|
||||
|
|
@ -48,7 +59,11 @@ nb_NO.strings = {
|
|||
dashboardTitle: 'Filopplaster',
|
||||
dashboardWindowTitle: 'Opplastingsvindu (Trykk Esc-knappen for å lukke)',
|
||||
dataUploadedOfTotal: '%{complete} av %{total}',
|
||||
dataUploadedOfUnknown: '%{complete} av ukjent',
|
||||
discardMediaFile: 'Forkast media',
|
||||
discardRecordedFile: 'Forkast opptak',
|
||||
done: 'Ferdig',
|
||||
dropHereOr: 'Slipp filer her eller %{browse}',
|
||||
dropHint: 'Dra filer hit',
|
||||
dropPasteBoth: 'Dra filer hit, %{browseFiles} eller %{browseFolders}',
|
||||
dropPasteFiles: 'Dra filer hit eller %{browseFiles}',
|
||||
|
|
@ -58,6 +73,7 @@ nb_NO.strings = {
|
|||
dropPasteImportFiles: 'Dra filer hit, %{browseFiles} eller importer fra:',
|
||||
dropPasteImportFolders: 'Dra filer hit, %{browseFolders} eller importer fra:',
|
||||
editFile: 'Rediger fil',
|
||||
editFileWithFilename: 'Rediger fil %{file}',
|
||||
editImage: 'Rediger bilde',
|
||||
editing: 'Redigerer %{file}',
|
||||
emptyFolderAdded: 'Ingen filer ble lagt til fra tom mappe',
|
||||
|
|
@ -66,11 +82,12 @@ nb_NO.strings = {
|
|||
'Ugyldig URL: Kontroller at adressen du angir er en direkte lenke til ei fil',
|
||||
enterTextToSearch: 'Skriv tekst for å søke etter bilder',
|
||||
enterUrlToImport: 'Angi URL for å importere fil',
|
||||
exceedsSize: 'Fila er større enn tillatt størrelse på %{size}',
|
||||
error: 'Feil',
|
||||
exceedsSize: '%{file} er større enn maksimal tillatt størrelse på %{size}',
|
||||
failedToAddFiles: 'Kunne ikke legge til filer',
|
||||
failedToFetch:
|
||||
'Companion kunne ikke hente denne URLen, kontroller at den er riktig',
|
||||
failedToUpload: 'Opplasting feilet for %{file}',
|
||||
fileSource: 'Filkilde: %{name}',
|
||||
filesUploadedOfTotal: {
|
||||
'0': '%{complete} av %{smart_count} fil opplastet',
|
||||
'1': '%{complete} av %{smart_count} filer opplastet',
|
||||
|
|
@ -82,24 +99,65 @@ nb_NO.strings = {
|
|||
'0': 'La til %{smart_count} fil fra %{folder}',
|
||||
'1': 'La til %{smart_count} filer fra %{folder}',
|
||||
},
|
||||
folderAlreadyAdded: 'Mappen "%{folder}" er allerede lagt til',
|
||||
generateImage: 'Generer bilde',
|
||||
generateImagePlaceholder:
|
||||
'En fredfull solnedgang over et fjellvann, med furutrær som speiler seg i vannet',
|
||||
generating1: 'KI tenker...',
|
||||
generating2: 'Knaser piksler...',
|
||||
generating3: 'Tilkaller bilder...',
|
||||
generating4: 'KI jobber...',
|
||||
generating5: 'Skaper magi...',
|
||||
generatingThumbnails: 'Genererer miniatyrbilde...',
|
||||
import: 'Importer',
|
||||
importFiles: 'Importer filer fra:',
|
||||
importFrom: 'Importer fra %{name}',
|
||||
inferiorSize: 'Fila er mindre enn tillatt størrelse på %{size}',
|
||||
loadedXFiles: 'Lastet inn %{numFiles} filer',
|
||||
loading: 'Laster...',
|
||||
logIn: 'Logg inn',
|
||||
logOut: 'Logg ut',
|
||||
micDisabled: 'Mikrofontilgang nektet av bruker',
|
||||
missingRequiredMetaField: 'Mangler påkrevde metafelter',
|
||||
missingRequiredMetaFieldOnFile: 'Mangler påkrevde metafelter i %{fileName}',
|
||||
missingRequiredMetaFields: {
|
||||
'0': 'Mangler påkrevd metafelt: %{fields}.',
|
||||
'1': 'Mangler påkrevde metafelter: %{fields}.',
|
||||
},
|
||||
myDevice: 'Min Enhet',
|
||||
noAudioDescription:
|
||||
'For å kunne ta opp lyd må du koble til en mikrofon eller en annen lydinngangsenhet',
|
||||
noAudioTitle: 'Mikrofon ikke tilgjengelig',
|
||||
noCameraDescription: 'Koble til kamera for å ta bilder eller ta opp video',
|
||||
noCameraTitle: 'Kamera ikke tilgjengelig',
|
||||
noDuplicates: "Kan ikke legge til '%{fileName}', da den allerede eksisterer",
|
||||
noFilesFound: 'Du har ingen filer eller mapper her',
|
||||
noInternetConnection: 'Ingen internettilgang',
|
||||
noMoreFilesAllowed: 'Kan ikke legge til nye filer mens opplasting pågår',
|
||||
noMoreFilesAllowed: 'Kan ikke legge til nye filer',
|
||||
noSearchResults: 'Dessverre er det ingen resultater for dette søket',
|
||||
openFolderNamed: 'Åpne mappe %{name}',
|
||||
pause: 'Pause',
|
||||
pauseUpload: 'Stopp opplasting midlertidig',
|
||||
paused: 'Midlertidig stoppet',
|
||||
pauseUpload: 'Stopp opplasting midlertidig',
|
||||
pickFiles: 'Velg filer',
|
||||
pickPhotos: 'Velg bilder',
|
||||
pleaseWait: 'Vennligst vent',
|
||||
pluginNameAudio: 'Lyd',
|
||||
pluginNameBox: 'Box',
|
||||
pluginNameCamera: 'Kamera',
|
||||
pluginNameDropbox: 'Dropbox',
|
||||
pluginNameFacebook: 'Facebook',
|
||||
pluginNameGoogleDrive: 'Google Drive',
|
||||
pluginNameGoogleDrivePicker: 'Google Drive',
|
||||
pluginNameGooglePhotosPicker: 'Google Photos',
|
||||
pluginNameOneDrive: 'OneDrive',
|
||||
pluginNameScreenCapture: 'Skjermopptak',
|
||||
pluginNameUnsplash: 'Unsplash',
|
||||
pluginNameUrl: 'Lenke',
|
||||
pluginNameWebdav: 'WebDAV',
|
||||
pluginNameZoom: 'Zoom',
|
||||
pluginWebdavInputLabel:
|
||||
'WebDAV-URL for en fil (f.eks. fra ownCloud eller Nextcloud)',
|
||||
poweredBy: 'Drevet av %{uppy}',
|
||||
processingXFiles: {
|
||||
'0': 'Prosesserer %{smart_count} fil',
|
||||
|
|
@ -109,37 +167,59 @@ nb_NO.strings = {
|
|||
recordingLength: 'Lengde på opptak: %{recording_length}',
|
||||
recordingStoppedMaxSize:
|
||||
'Opptak stoppet fordi fila er i ferd med å overskride tillatt størrelse',
|
||||
recordVideoBtn: 'Ta opp video',
|
||||
recoveredAllFiles:
|
||||
'Vi gjenopprettet alle filer. Du kan nå fortsette opplastingen.',
|
||||
recoveredXFiles: {
|
||||
'0': 'Vi kunne ikke gjenopprette 1 fil helt. Velg den på nytt og fortsett opplastingen.',
|
||||
'1': 'Vi kunne ikke gjenopprette %{smart_count} filer helt. Velg dem på nytt og fortsett opplastingen.',
|
||||
},
|
||||
removeFile: 'Fjern fil',
|
||||
reSelect: 'Velg på nytt',
|
||||
resetFilter: 'Nullstill filter',
|
||||
resetSearch: 'Nullstill søk',
|
||||
resume: 'Fortsett',
|
||||
resumeUpload: 'Fortsett opplasting',
|
||||
retry: 'Prøv igjen',
|
||||
retryUpload: 'Prøv igjen',
|
||||
revert: 'Tilbakestill',
|
||||
rotate: 'Roter',
|
||||
rotate: 'Roter 90°',
|
||||
save: 'Lagre',
|
||||
saveChanges: 'Lagre endringer',
|
||||
search: 'Søk',
|
||||
searchImages: 'Søk etter bilder',
|
||||
selectFileNamed: 'Velg fil %{name}',
|
||||
selectX: {
|
||||
'0': 'Velg %{smart_count}',
|
||||
'1': 'Velg %{smart_count}',
|
||||
},
|
||||
sessionRestored: 'Økt gjenopprettet',
|
||||
showErrorDetails: 'Vis feildetaljer',
|
||||
signInWithGoogle: 'Logg inn med Google',
|
||||
smile: 'Smil!',
|
||||
startAudioRecording: 'Start lydopptak',
|
||||
startCapturing: 'Start skjermopptak',
|
||||
startRecording: 'Start videoopptak',
|
||||
stopAudioRecording: 'Stopp lydopptak',
|
||||
stopCapturing: 'Stopp skjermopptak',
|
||||
stopRecording: 'Stopp videoopptak',
|
||||
streamActive: 'Strøm aktiv',
|
||||
streamPassive: 'Strøm passiv',
|
||||
submitRecordedFile: 'Send inn opptak',
|
||||
takePicture: 'Ta bilde',
|
||||
timedOut: 'Opplasting stoppet for %{seconds} sekunder, avbryter.',
|
||||
unselectFileNamed: 'Fjern markering for %{name}',
|
||||
takePictureBtn: 'Ta bilde',
|
||||
takeScreenshot: 'Ta skjermbilde',
|
||||
unnamed: 'Uten navn',
|
||||
upload: 'Last opp',
|
||||
uploadComplete: 'Opplasting ferdig',
|
||||
uploadFailed: 'Opplasting feilet',
|
||||
uploading: 'Laster opp',
|
||||
uploadingXFiles: {
|
||||
'0': 'Laster opp %{smart_count} fil',
|
||||
'1': 'Laster opp %{smart_count} filer',
|
||||
},
|
||||
uploadPaused: 'Opplasting midlertidig stoppet',
|
||||
uploadStalled:
|
||||
'Opplastingen har ikke gjort fremgang på %{seconds} sekunder. Du vil kanskje prøve på nytt.',
|
||||
uploadXFiles: {
|
||||
'0': 'Last opp %{smart_count} fil',
|
||||
'1': 'Last opp %{smart_count} filer',
|
||||
|
|
@ -148,11 +228,6 @@ nb_NO.strings = {
|
|||
'0': 'Last opp +%{smart_count} fil',
|
||||
'1': 'Last opp +%{smart_count} filer',
|
||||
},
|
||||
uploading: 'Laster opp',
|
||||
uploadingXFiles: {
|
||||
'0': 'Laster opp %{smart_count} fil',
|
||||
'1': 'Laster opp %{smart_count} filer',
|
||||
},
|
||||
xFilesSelected: {
|
||||
'0': '%{smart_count} fil valgt',
|
||||
'1': '%{smart_count} filer valgt',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,51 @@
|
|||
import Core from '@uppy/core'
|
||||
import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import Core, { type UppyEventMap } from '@uppy/core'
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import Tus, { type TusBody } from './index.js'
|
||||
|
||||
// Shared fake XHR object — must be declared via vi.hoisted so it's available
|
||||
// inside the vi.mock factory (which is hoisted before imports).
|
||||
const { fakeXhr } = vi.hoisted(() => ({
|
||||
fakeXhr: {
|
||||
status: 403,
|
||||
responseText: JSON.stringify({
|
||||
message: 'File cannot be uploaded as the BIN content type is disallowed!',
|
||||
status_code: 403,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock tus-js-client so the upload-error test never touches the network.
|
||||
// The mock Upload fires onError immediately with a fake DetailedError.
|
||||
vi.mock('tus-js-client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('tus-js-client')>()
|
||||
class MockUpload {
|
||||
private options: Record<string, any>
|
||||
|
||||
constructor(_file: any, options: Record<string, any>) {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
start() {
|
||||
const err = Object.assign(new Error('tus: server responded with 403'), {
|
||||
originalResponse: {
|
||||
getStatus: () => 403,
|
||||
getUnderlyingObject: () => fakeXhr,
|
||||
},
|
||||
originalRequest: null,
|
||||
})
|
||||
setTimeout(() => this.options.onError(err), 0)
|
||||
}
|
||||
|
||||
abort() {}
|
||||
|
||||
// ponytail: tus calls this before start(); return empty so no resume logic runs
|
||||
findPreviousUploads() {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
}
|
||||
return { ...actual, Upload: MockUpload }
|
||||
})
|
||||
|
||||
describe('Tus', () => {
|
||||
it('Throws errors if autoRetry option is true', () => {
|
||||
const uppy = new Core()
|
||||
|
|
@ -44,4 +88,40 @@ describe('Tus', () => {
|
|||
{ xhr: XMLHttpRequest } | undefined
|
||||
>()
|
||||
})
|
||||
|
||||
describe('upload-error response', () => {
|
||||
it('sends the server response over the upload-error event', async () => {
|
||||
const core = new Core<any, TusBody>()
|
||||
core.use(Tus, {
|
||||
endpoint: 'https://fake-endpoint.uppy.io/files/',
|
||||
retryDelays: [],
|
||||
})
|
||||
const id = core.addFile({
|
||||
type: 'application/octet-stream',
|
||||
source: 'test',
|
||||
name: 'test.bin',
|
||||
data: new Blob([new Uint8Array(1024)]),
|
||||
})
|
||||
|
||||
const event = new Promise<
|
||||
Parameters<UppyEventMap<any, TusBody>['upload-error']>
|
||||
>((resolve) => {
|
||||
core.once('upload-error', (...args) => resolve(args))
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
core.upload().catch(() => {
|
||||
// Core rejects the upload; we assert on the event/state instead.
|
||||
}),
|
||||
event.then(([, , response]) => {
|
||||
expect(response?.status).toBe(403)
|
||||
expect(JSON.parse(response!.body!.xhr.responseText).message).toBe(
|
||||
'File cannot be uploaded as the BIN content type is disallowed!',
|
||||
)
|
||||
}),
|
||||
])
|
||||
|
||||
expect(core.getFile(id).response?.status).toBe(403)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -164,13 +164,28 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
* Clean up all references for a file's upload: the tus.Upload instance,
|
||||
* any events related to the file, and the Companion WebSocket connection.
|
||||
*/
|
||||
resetUploaderReferences(fileID: string, opts?: { abort: boolean }): void {
|
||||
resetUploaderReferences(
|
||||
fileID: string,
|
||||
opts?: {
|
||||
/** Terminate the upload on the server (sends a `DELETE` request). */
|
||||
abort?: boolean
|
||||
/**
|
||||
* Abort the underlying request. Defaults to `true`. Set to `false` when
|
||||
* the request has already completed (e.g. in the error handler), so the
|
||||
* underlying `xhr` — and thus the server response — is preserved instead
|
||||
* of being reset by `abort()`.
|
||||
*/
|
||||
abortRequest?: boolean
|
||||
},
|
||||
): void {
|
||||
const uploader = this.uploaders[fileID]
|
||||
if (uploader) {
|
||||
uploader.abort()
|
||||
if (opts?.abortRequest !== false) {
|
||||
uploader.abort()
|
||||
|
||||
if (opts?.abort) {
|
||||
uploader.abort(true)
|
||||
if (opts?.abort) {
|
||||
uploader.abort(true)
|
||||
}
|
||||
}
|
||||
|
||||
this.uploaders[fileID] = null
|
||||
|
|
@ -219,6 +234,12 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
): Promise<tus.Upload | string> {
|
||||
this.resetUploaderReferences(file.id)
|
||||
|
||||
// Captured in `onError` and forwarded to the `upload-error` event in the
|
||||
// `.catch` below, so consumers can read the failing server response.
|
||||
let errorResponse:
|
||||
| Omit<NonNullable<UppyFile<M, B>['response']>, 'uploadURL'>
|
||||
| undefined
|
||||
|
||||
// Create a new tus upload
|
||||
return new Promise<tus.Upload | string>((resolve, reject) => {
|
||||
let queuedRequest: ReturnType<RateLimitedQueue['run']>
|
||||
|
|
@ -291,6 +312,23 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
uploadOptions.onError = (err) => {
|
||||
this.uppy.log(err)
|
||||
|
||||
// tus-js-client only calls `onError` once it has given up retrying, so
|
||||
// the request has already completed. Capture the server response (status
|
||||
// + body) and forward it to the `upload-error` event and `file.response`,
|
||||
// mirroring the shape emitted by `onSuccess`.
|
||||
const originalResponse = (err as tus.DetailedError).originalResponse
|
||||
if (originalResponse != null) {
|
||||
errorResponse = {
|
||||
status: originalResponse.getStatus(),
|
||||
body: {
|
||||
// We have to put `as XMLHttpRequest` because tus-js-client
|
||||
// returns `any`, as the type differs in Node.js and the browser.
|
||||
// In the browser it's always `XMLHttpRequest`.
|
||||
xhr: originalResponse.getUnderlyingObject() as XMLHttpRequest,
|
||||
} as unknown as B,
|
||||
}
|
||||
}
|
||||
|
||||
const xhr =
|
||||
(err as tus.DetailedError).originalRequest != null
|
||||
? (err as tus.DetailedError).originalRequest.getUnderlyingObject()
|
||||
|
|
@ -299,7 +337,11 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
err = new NetworkError(err, xhr)
|
||||
}
|
||||
|
||||
this.resetUploaderReferences(file.id)
|
||||
// Do not abort the request here: it has already completed, and aborting
|
||||
// it would reset the underlying `xhr` (status `0`, empty body) and
|
||||
// discard the response we just captured. We still drop our references
|
||||
// and remove the event listeners.
|
||||
this.resetUploaderReferences(file.id, { abortRequest: false })
|
||||
queuedRequest?.abort()
|
||||
|
||||
if (typeof opts.onError === 'function') {
|
||||
|
|
@ -516,7 +558,10 @@ export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
|
|||
queuedRequest = this.requests.run(qRequest)
|
||||
})
|
||||
}).catch((err) => {
|
||||
this.uppy.emit('upload-error', file, err)
|
||||
// `errorResponse` is captured in the `onError` handler above (the request
|
||||
// is intentionally not aborted there), so the server response is still
|
||||
// available here to forward to the `upload-error` event.
|
||||
this.uppy.emit('upload-error', file, err, errorResponse)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue