Merge branch 'main' into idb-getsize-cache

This commit is contained in:
Prakash 2026-05-14 18:30:48 +05:30 committed by GitHub
commit ed50d93ec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 576 additions and 1267 deletions

View file

@ -0,0 +1,9 @@
---
"@uppy/components": major
"@uppy/companion": major
"uppy": major
"@uppy/transloadit": minor
"@uppy/locales": minor
---
Remove @uppy/instagram references from all the packages

View file

@ -0,0 +1,5 @@
---
"@uppy/transloadit": minor
---
Add assemblyStatus and lastAssemblyStatus to transloadit's plugin state

View file

@ -0,0 +1,8 @@
---
"@uppy/companion-client": major
"@uppy/companion": major
---
Send token using websocket instead of window.opener.
Breaking in `@uppy/companion-client` because it needs newest version of Companion in order to work.
Breaking in `@uppy/companion` because `companion.socket()` now requires `companionOptions` to be passed as the second argument.

View file

@ -44,9 +44,6 @@ COMPANION_DROPBOX_SECRET=***
COMPANION_GOOGLE_KEY=***
COMPANION_GOOGLE_SECRET=***
COMPANION_INSTAGRAM_KEY=***
COMPANION_INSTAGRAM_SECRET=***
COMPANION_FACEBOOK_KEY=***
COMPANION_FACEBOOK_SECRET=***

View file

@ -57,7 +57,7 @@ This is not needed for changing existing components.
## Companion
If youd like to work on features that the basic development version of Uppy
doesnt support, such as Uppy integrations with Instagram/Google Drive/Facebook
doesnt support, such as Uppy integrations with Google Drive/Facebook
etc., you need to set up your `.env` file (copy the contents of `.env.example`
and adjust them based on what you need to work on), and run:
@ -78,7 +78,7 @@ when files are changed.
### How the Authentication and Token mechanism works
This section describes how Authentication works between Companion and Providers.
While this behaviour is the same for all Providers (Dropbox, Instagram, Google
While this behaviour is the same for all Providers (Dropbox, Google
Drive, etc.), we are going to be referring to Dropbox in place of any Provider
throughout this section.

View file

@ -42,7 +42,7 @@ jobs:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn install
corepack yarn install --immutable
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0

View file

@ -47,7 +47,7 @@ jobs:
node-version: ${{matrix.node-version}}
- name: Install dependencies
run:
corepack yarn install
corepack yarn install --immutable
- name: Install Playwright Browsers
run: corepack yarn workspace @uppy/dashboard playwright install --with-deps
- name: Build
@ -84,7 +84,7 @@ jobs:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn install
corepack yarn install --immutable
- run: corepack yarn run typecheck
lint_js:

View file

@ -38,35 +38,49 @@ jobs:
path: /tmp/companion-${{ github.sha }}.tar.gz
docker:
name: DockerHub
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: 0
COMPOSE_DOCKER_CLI_BUILD: 0
name: DockerHub (${{ matrix.platform }})
strategy:
matrix:
include:
- runner: ubuntu-24.04
platform: linux/amd64
platform_tag: amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
platform_tag: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Docker meta
id: docker_meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: transloadit/companion
tags: |
type=edge
type=raw,value=latest,enable=false
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
push: true
context: .
platforms: linux/amd64,linux/arm64
platforms: ${{ matrix.platform }}
file: Dockerfile
tags: ${{ steps.docker_meta.outputs.tags }}
labels: ${{ steps.docker_meta.outputs.labels }}
tags: transloadit/companion:edge-${{ matrix.platform_tag }}
docker-manifest:
name: DockerHub manifest
needs: docker
runs-on: ubuntu-latest
steps:
- uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Publish edge manifest
run: |
docker buildx imagetools create \
--tag transloadit/companion:edge \
transloadit/companion:edge-amd64 \
transloadit/companion:edge-arm64

View file

@ -30,18 +30,6 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
uses: actions/setup-node@v6
with:

View file

@ -9,5 +9,6 @@ initScope: uppy
nodeLinker: node-modules
npmPublishAccess: public
npmMinimalAgeGate: 2880
npmPublishAccess: public

View file

@ -83,7 +83,7 @@ inline into the page. This, and many more configuration options can be found
here: <https://uppy.io/docs/dashboard/>.
Uppy has many more Plugins besides Xhr and the Dashboard. For example, you can
enable Webcam, Instagram, or video encoding support. Note that for some Plugins,
enable Webcam or video encoding support. Note that for some Plugins,
you will need to run a server side component called: Companion. Those plugins
are marked with a (c) symbol. Alternatively, you can sign up for a free
Transloadit account. Transloadit runs Companion for you, tusd servers to handle

View file

@ -46,7 +46,7 @@ yarn workspace @uppy/[package-name] build
- **Uppy Core** (`@uppy/core`) - Main class that manages plugins, state, and events
- **Plugins** - Modular components for different functionalities:
- **UI Plugins**: Dashboard, Drag & Drop, File Input, Webcam, etc.
- **Provider Plugins**: Google Drive, Dropbox, Instagram, etc. (require Companion)
- **Provider Plugins**: Google Drive, Dropbox, etc. (require Companion)
- **Uploader Plugins**: Tus (resumable), XHR Upload, AWS S3, etc.
- **Utility Plugins**: Golden Retriever (recovery), Thumbnail Generator, etc.

View file

@ -7,7 +7,7 @@ with any application. Its fast, has a comprehensible API and lets you worry
about more important problems than building a file uploader.
- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Box,
Instagram or snap and record selfies with a camera
or snap and record selfies with a camera
- **Preview** and edit metadata with a nice interface
- **Upload** to the final destination, optionally process/encode
@ -82,7 +82,7 @@ For the supported frameworks (except Angular) Uppy offers three ways to build us
- Lightweight, modular plugin-based architecture, light on dependencies :zap:
- Resumable file uploads via the open [tus](https://tus.io/) standard, so large
uploads survive network hiccups
- Supports picking files from: Webcam, Dropbox, Box, Google Drive, Instagram,
- Supports picking files from: Webcam, Dropbox, Box, Google Drive,
bypassing the users device where possible, syncing between servers directly
via [@uppy/companion](https://uppy.io/docs/companion)
- Works great with file encoding and processing backends, such as
@ -140,7 +140,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
- [Uppy](https://uppy.io/docs/uppy/) — full list of options, methods and events
- [Companion](https://uppy.io/docs/companion/) — setting up and running a
Companion instance, which adds support for Instagram, Dropbox, Box, Google
Companion instance, which adds support for Dropbox, Box, Google
Drive and remote URLs
- [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins
with React apps
@ -153,7 +153,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
- [`Dashboard`](https://uppy.io/docs/dashboard/) — universal UI with previews,
progress bars, metadata editor and all the cool stuff. Required for most UI
plugins like Webcam and Instagram
plugins like Webcam
- Headless components ([react](https://uppy.io/docs/react/), [svelte](https://uppy.io/docs/svelte/), [vue](https://uppy.io/docs/vue/))
### Sources
@ -166,8 +166,6 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
Google Drive
- ⓒ [`Dropbox`](https://uppy.io/docs/dropbox/) — import files from Dropbox
- ⓒ [`Box`](https://uppy.io/docs/box/) — import files from Box
- ⓒ [`Instagram`](https://uppy.io/docs/instagram/) — import images and videos
from Instagram
- ⓒ [`Facebook`](https://uppy.io/docs/facebook/) — import images and videos from
Facebook
- ⓒ [`OneDrive`](https://uppy.io/docs/onedrive/) — import files from Microsoft
@ -222,7 +220,7 @@ cases, the file input leaves some to be desired:
- Uppy supports editing meta information before uploading.
- Uppy allows cropping images before uploading.
- Theres the situation where people are using their mobile devices and want to
upload on the go, but they have their picture on Instagram, files in Dropbox
upload on the go, but they have files in Dropbox
or a plain file URL from anywhere on the open web. Uppy allows to pick files
from those and push it to the destination without downloading it to your
mobile device first.
@ -272,7 +270,7 @@ If you want resumability with the Tus plugin, use
[one of the tus server implementations](https://tus.io/implementations.html) 👌🏼
And youll need [`@uppy/companion`](https://uppy.io/docs/companion) if youd
like your users to be able to pick files from Instagram, Google Drive, Dropbox
like your users to be able to pick files from Google Drive, Dropbox
or via direct URLs (with more services coming).
## Contributions are welcome

View file

@ -16,17 +16,12 @@
import {
Uppy,
Dashboard,
Instagram,
GoogleDrive,
Tus,
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
const uppy = new Uppy({ debug: true, autoProceed: false })
.use(Dashboard, { trigger: '#uppyModalOpener' })
.use(Instagram, {
target: Dashboard,
companionUrl: 'http://localhost:3020',
})
.use(GoogleDrive, {
target: Dashboard,
companionUrl: 'http://localhost:3020',

View file

@ -32,10 +32,6 @@ const companionOptions = {
key: 'your google key',
secret: 'your google secret',
},
instagram: {
key: 'your instagram key',
secret: 'your instagram secret',
},
dropbox: {
key: 'your dropbox key',
secret: 'your dropbox secret',

View file

@ -129,6 +129,5 @@ describe('RemoteSource Component', () => {
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -132,7 +132,6 @@ describe('RemoteSource Component', () => {
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -131,6 +131,5 @@ describe('RemoteSource Component', () => {
await expect.element(loginButton).toBeInTheDocument()
await loginButton.click()
await expect.element(loginButton).toBeInTheDocument()
})
})

View file

@ -2,7 +2,7 @@
"name": "@uppy-dev/build",
"version": "0.0.0",
"private": true,
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox, Google Drive, S3 and more :dog:",
"license": "MIT",
"workspaces": [
"examples/*",

View file

@ -5,8 +5,11 @@ import type {
UnknownProviderPlugin,
Uppy,
} from '@uppy/core'
import type { CompanionClientProvider, RequestOptions } from '@uppy/utils'
import { isOriginAllowed } from './getAllowedHosts.js'
import {
type CompanionClientProvider,
getSocketHost,
type RequestOptions,
} from '@uppy/utils'
import type { CompanionPluginOptions } from './index.js'
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
@ -135,22 +138,27 @@ export default class Provider<M extends Meta, B extends Body>
authUrl({
authFormData,
query,
authCallbackToken,
}: {
authFormData: unknown
query: Record<string, string>
authCallbackToken?: string | undefined
}): string {
const params = new URLSearchParams({
const searchParams = new URLSearchParams({
...query,
// This is only used for Companion instances configured to accept multiple origins.
// `origin` is only used for Companion instances configured to accept multiple origins.
state: btoa(JSON.stringify({ origin: getOrigin() })),
...this.authQuery({ authFormData }),
})
if (this.preAuthToken) {
params.set('uppyPreAuthToken', this.preAuthToken)
searchParams.set('uppyPreAuthToken', this.preAuthToken)
}
if (authCallbackToken) {
searchParams.set('authCallbackToken', authCallbackToken)
}
return `${this.hostname}/${this.id}/connect?${params}`
return `${this.hostname}/${this.id}/connect?${searchParams}`
}
protected async loginSimpleAuth({
@ -181,67 +189,62 @@ export default class Provider<M extends Meta, B extends Body>
signal: AbortSignal
}): Promise<void> {
await this.ensurePreAuth()
signal.throwIfAborted()
const link = this.authUrl({ query: { uppyVersions }, authFormData })
// Important: We need to do this synchronously, or else browsers might block the popup.
// (We cannot wait until the websocket connection is established).
// This opens up a race condtition if the websocket is not connected before the user
// completes(or cancels) authentication, but thats a small compromose we gotta make.
const authCallbackToken = crypto.randomUUID()
const link = this.authUrl({
query: { uppyVersions },
authFormData,
authCallbackToken,
})
const authWindow = window.open(link, '_blank')
let interval: number | undefined
let handleMessage: ((e: MessageEvent<any>) => void) | undefined
let webSocket: WebSocket | undefined
try {
return await new Promise((resolve, reject) => {
handleMessage = (e: MessageEvent<any>) => {
if (e.source !== authWindow) {
let jsonData = ''
try {
// TODO improve our uppy logger so that it can take an arbitrary number of arguments,
// each either objects, errors or strings,
// then we dont have to manually do these things like json stringify when logging.
// the logger should never throw an error.
jsonData = JSON.stringify(e.data)
} catch (_err) {
// in case JSON.stringify fails (ignored)
const host = getSocketHost(this.opts.companionUrl)
// Note that this promise is not guaranteed to settle in all cases
const token = await new Promise<string>((resolve, reject) => {
webSocket = new WebSocket(
`${host}/api2/auth-callback/token/${authCallbackToken}`,
)
webSocket.addEventListener('close', () => {
reject(new Error('Socket closed'))
})
webSocket.addEventListener('error', (error) => {
this.uppy.log(
`Companion socket error ${JSON.stringify(error)}, closing socket`,
'warning',
)
webSocket?.close() // 'close' event will be emitted
})
webSocket.addEventListener('message', (e) => {
try {
const { token, error } = JSON.parse(e.data)
if (error) {
reject(new Error('Authentication reported error'))
} else if (!token) {
reject(new Error('Authentication did not return a token'))
} else {
resolve(token)
}
this.uppy.log(
`ignoring event from unknown source ${jsonData}`,
'warning',
)
return
} catch (err) {
reject(err)
}
})
const { companionAllowedHosts } = this.#getPlugin().opts
if (!isOriginAllowed(e.origin, companionAllowedHosts)) {
this.uppy.log(
`ignoring event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`,
'warning',
)
// We cannot reject here because the page might send events from other origins
// before sending the "real" auth completed event.
// for example Box has a "Pendo" tool that sends events to the opener
// https://github.com/transloadit/uppy/pull/5719
return
}
// Check if it's a string before doing the JSON.parse to maintain support
// for older Companion versions that used object references
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
if (data.error) {
const { uppy } = this
const message = uppy.i18n('authAborted')
uppy.info({ message }, 'warning', 5000)
reject(new Error('auth aborted'))
return
}
if (!data.token) {
reject(new Error('did not receive token from auth window'))
return
}
resolve(this.setAuthToken(data.token))
}
signal.addEventListener('abort', () =>
reject(new Error('Authentication was aborted')),
)
// poll for user closure of the window, so we can reject when it happens
if (authWindow) {
@ -251,15 +254,21 @@ export default class Provider<M extends Meta, B extends Body>
}
}, 500)
}
signal.addEventListener('abort', () => reject(new Error('Aborted')))
window.addEventListener('message', handleMessage)
})
this.setAuthToken(token)
} catch (err) {
const message = this.uppy.i18n('authAborted')
this.uppy.info({ message }, 'warning', 5000)
this.uppy.log(`Authentication failed: ${err.message}`, 'warning')
throw err
} finally {
// cleanup:
authWindow?.close()
window.clearInterval(interval)
if (handleMessage) window.removeEventListener('message', handleMessage)
// if we don't setTimeout, the window doesn't really close (I don't know why).
setTimeout(() => authWindow?.close(), 1)
this.uppy.log(`Closing auth callback socket ${authCallbackToken}`)
webSocket?.close()
clearInterval(interval)
}
}

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import getAllowedHosts, { isOriginAllowed } from './getAllowedHosts.js'
import getAllowedHosts from './getAllowedHosts.js'
describe('getAllowedHosts', () => {
it('can convert companionAllowedHosts', () => {
@ -30,20 +30,3 @@ describe('getAllowedHosts', () => {
)
})
})
describe('isOriginAllowed', () => {
it('should check origin', () => {
expect(isOriginAllowed('a', [/^.+$/])).toBeTruthy()
expect(isOriginAllowed('a', ['^.+$'])).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['^www\\.transloadit\\.com$']),
).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['^transloadit\\.com$']),
).toBeFalsy()
expect(isOriginAllowed('match', ['fail', 'match'])).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['\\.transloadit\\.com$']),
).toBeTruthy()
})
})

View file

@ -47,15 +47,3 @@ export default function getAllowedHosts(
ret = escapeRegex(ret)
return ret
}
export function isOriginAllowed(
origin: string,
allowedOrigin: string | RegExp | Array<string | RegExp> | undefined,
): boolean {
const patterns = Array.isArray(allowedOrigin)
? allowedOrigin.map(wrapInRegex)
: [wrapInRegex(allowedOrigin)]
return patterns.some(
(pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`),
) // allowing for trailing '/'
}

View file

@ -38,8 +38,6 @@ data:
COMPANION_BOX_SECRET: 'YOUR BOX SECRET'
COMPANION_GOOGLE_KEY: 'YOUR GOOGLE KEY'
COMPANION_GOOGLE_SECRET: 'YOUR GOOGLE SECRET'
COMPANION_INSTAGRAM_KEY: 'YOUR INSTAGRAM KEY'
COMPANION_INSTAGRAM_SECRET: 'YOUR INSTAGRAM SECRET'
COMPANION_AWS_KEY: 'YOUR AWS KEY'
COMPANION_AWS_SECRET: 'YOUR AWS SECRET'
COMPANION_AWS_BUCKET: 'YOUR AWS S3 BUCKET'

View file

@ -8,7 +8,7 @@ Companion is a server integration for
[Uppy](https://github.com/transloadit/uppy) file uploader.
It handles the server-to-server communication between your server and file
storage providers such as Google Drive, Dropbox, Instagram, etc. **Companion is
storage providers such as Google Drive, Dropbox, etc. **Companion is
not a target to upload files to**. For this, use a <https://tus.io> server (if
you want resumable) or your existing Apache/Nginx server (if you dont).
[See here for full documentation](https://uppy.io/docs/companion/)

View file

@ -26,10 +26,6 @@ COMPANION_GOOGLE_KEY=
COMPANION_GOOGLE_SECRET=
COMPANION_GOOGLE_SECRET_FILE=
COMPANION_INSTAGRAM_KEY=
COMPANION_INSTAGRAM_SECRET=
COMPANION_INSTAGRAM_SECRET_FILE=
COMPANION_FACEBOOK_KEY=
COMPANION_FACEBOOK_SECRET=
COMPANION_FACEBOOK_SECRET_FILE=

View file

@ -56,10 +56,6 @@ export default function grantConfig(): GrantStaticConfig {
access_url: 'https://api.box.com/oauth2/token',
callback: '/box/callback',
},
instagram: {
...defaults,
callback: '/instagram/callback',
},
facebook: {
...defaults,
scope: ['email', 'user_photos'],

View file

@ -222,7 +222,7 @@ export default class Uploader {
/**
* Uploads file to destination based on the supplied protocol (tus, s3-multipart, multipart)
* For tus uploads, the deferredLength option is enabled, because file size value can be unreliable
* for some providers (Instagram particularly)
* for some providers.
*
* @param optionsIn
*/

View file

@ -3,32 +3,23 @@
*/
import type { NextFunction, Request, Response } from 'express'
import serialize from 'serialize-javascript'
import emitter from '../emitter/index.js'
import {
authCallbackErrorHtml,
legacyAuthCallbackHtml,
} from '../helpers/html.js'
import * as tokenService from '../helpers/jwt.js'
import * as oAuthState from '../helpers/oauth-state.js'
import logger from '../logger.js'
const closePageHtml = (origin: string | undefined) => `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
// if window.opener is nullish, we want the following line to throw to avoid
// the window closing without informing the user.
window.opener.postMessage(${serialize({ error: true })}, ${serialize(origin)})
window.close()
</script>
</head>
<body>Authentication failed.</body>
</html>`
export default function callback(
req: Request,
res: Response,
next: NextFunction,
): void {
const providerName = req.params['providerName']
const { companion } = req
if (providerName == null || providerName.length === 0) {
res.sendStatus(400)
return
@ -51,7 +42,25 @@ export default function callback(
req.id,
)
logger.debug(req.session?.grant?.response, 'callback.oauth.resp', req.id)
res.status(400).send(closePageHtml(originString))
const authCallbackToken =
grantDynamic.state &&
oAuthState.getFromState(
grantDynamic.state,
'authCallbackToken',
companion.options.secret,
)
// only new Uppy clients will set an authCallbackToken in the state
// in that case, we send the token through the emitter.
if (authCallbackToken) {
emitter().emit(authCallbackToken, { error: true })
res.status(400).send(authCallbackErrorHtml())
} else {
// This is backwards compatible with old Uppy clients:
res
.status(400)
.send(legacyAuthCallbackHtml({ error: true }, originString))
}
return
}

View file

@ -123,6 +123,11 @@ export default function connect(
stateObj.preAuthToken = preAuthTokenValue
}
const authCallbackToken = req.query['authCallbackToken']
if (typeof authCallbackToken === 'string') {
stateObj.authCallbackToken = authCallbackToken
}
// Get the computed header generated by `cors` in a previous middleware.
stateObj.origin = res.getHeader('Access-Control-Allow-Origin')
let clientOrigin: string | undefined

View file

@ -1,45 +1,12 @@
import type { NextFunction, Request, Response } from 'express'
import serialize from 'serialize-javascript'
import emitter from '../emitter/index.js'
import {
authCallbackSuccessHtml,
legacyAuthCallbackHtml,
} from '../helpers/html.js'
import * as oAuthState from '../helpers/oauth-state.js'
import { isOriginAllowed } from './connect.js'
const htmlContent = (token: string, origin: string): string => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
(function() {
'use strict';
var data = ${serialize({ token })};
var origin = ${serialize(origin)};
if (window.opener != null) {
window.opener.postMessage(data, origin);
window.close();
} else {
// maybe this will work? (note that it's not possible to try/catch this to see whether it worked)
window.postMessage(data, origin);
console.warn('Unable to send the authentication token to the web app. This probably means that the web app was served from a HTTP server that includes the \`Cross-Origin-Opener-Policy: same-origin\` header. Make sure that the Uppy app is served from a server that does not send this header, or set to \`same-origin-allow-popups\`.');
addEventListener("DOMContentLoaded", function() {
document.body.appendChild(document.createTextNode('Something went wrong. Please contact the site administrator. You may now exit this page.'));
});
}
})();
</script>
</head>
<body>
<noscript>
JavaScript must be enabled for this to work.
</noscript>
</body>
</html>`
}
export default function sendToken(
req: Request,
res: Response,
@ -74,5 +41,18 @@ export default function sendToken(
return
}
res.send(htmlContent(`${uppyAuthToken}`, clientOrigin))
const authCallbackToken = oAuthState.getFromState(
state,
'authCallbackToken',
companion.options.secret,
)
// only new Uppy clients will set an authCallbackToken in the state
// in that case, we send the token through the emitter.
if (authCallbackToken) {
emitter().emit(authCallbackToken, { token: uppyAuthToken })
res.send(authCallbackSuccessHtml())
} else {
// This is backwards compatible with old Uppy clients:
res.send(legacyAuthCallbackHtml({ token: uppyAuthToken }, clientOrigin))
}
}

View file

@ -0,0 +1,92 @@
import serialize from 'serialize-javascript'
export const authCallbackSuccessHtml = () => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
(function() {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
window.close();
});
})();
</script>
</head>
<body>
<center>Authentication successful. You may now close this page.</center>
</body>
</html>`
}
export const authCallbackErrorHtml = () => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
(function() {
'use strict';
document.addEventListener('DOMContentLoaded', function () {
window.close();
});
})();
</script>
</head>
<body>
<center>Authentication failed. You may now close this page.</center>
</body>
</html>`
}
/**
* Generate an HTML page that will post a message to the opener window and then close itself.
* This is only used by old Uppy clients
*
* @param {unknown} data data payload
* @param {string} origin url string
*/
export const legacyAuthCallbackHtml = (
data: unknown,
origin: string | undefined,
) => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
(function() {
'use strict';
var data = ${serialize(data)};
var origin = ${serialize(origin)};
if (window.opener != null) {
window.opener.postMessage(data, origin);
window.close();
} else {
// maybe this will work? (note that it's not possible to try/catch this to see whether it worked)
window.postMessage(data, origin);
console.warn('Unable to send the authentication token to the web app. This probably means that the web app was served from a HTTP server that includes the \`Cross-Origin-Opener-Policy: same-origin\` header. Make sure that the Uppy app is served from a server that does not send this header, or set to \`same-origin-allow-popups\`.');
addEventListener("DOMContentLoaded", function() {
document.body.appendChild(document.createTextNode('Something went wrong. Please contact the site administrator. You may now exit this page.'));
});
}
})();
</script>
</head>
<body>
<noscript>
JavaScript must be enabled for this to work.
</noscript>
</body>
</html>`
}

View file

@ -9,6 +9,7 @@ export type OAuthState = {
preAuthToken?: string
companionInstance?: string
customerDefinedAllowedOrigins?: string[]
authCallbackToken?: string
}
export const encodeState = (

View file

@ -7,7 +7,6 @@ import { getCredentialsResolver } from './credentials.js'
import dropbox from './dropbox/index.js'
import facebook from './facebook/index.js'
import { Drive } from './google/drive/index.js'
import instagram from './instagram/graph/index.js'
import onedrive from './onedrive/index.js'
import { isOAuthProvider, type ProviderCtor } from './Provider.js'
import unsplash from './unsplash/index.js'
@ -113,7 +112,6 @@ export function getDefaultProviders() {
facebook,
onedrive,
zoom,
instagram,
unsplash,
webdav,
}

View file

@ -1,103 +0,0 @@
import querystring from 'node:querystring'
const MEDIA_TYPES = Object.freeze({
video: 'VIDEO',
carousel: 'CAROUSEL_ALBUM',
image: 'IMAGE',
})
type InstagramMedia = {
id: string
media_type: string
media_url?: string
thumbnail_url?: string
timestamp: string
children?: { data: InstagramMedia[] }
}
type InstagramListResponse = {
data: InstagramMedia[]
paging?: { cursors?: { after?: string } }
}
const isVideo = (item: InstagramMedia): boolean =>
item.media_type === MEDIA_TYPES.video
const isFolder = (_item: InstagramMedia): boolean => {
return false
}
const getItemIcon = (item: InstagramMedia): string | undefined => {
return isVideo(item) ? item.thumbnail_url : item.media_url
}
const getItemSubList = (item: InstagramListResponse): InstagramMedia[] => {
const newItems: InstagramMedia[] = []
item.data.forEach((subItem) => {
if (subItem.media_type === MEDIA_TYPES.carousel) {
subItem.children?.data.forEach((i) => newItems.push(i))
} else {
newItems.push(subItem)
}
})
return newItems
}
const getItemName = (item: InstagramMedia, index: number): string => {
const ext = isVideo(item) ? 'mp4' : 'jpeg'
// adding index, so the name is unique
return `Instagram ${item.timestamp}${index}.${ext}`
}
const getMimeType = (item: InstagramMedia): string => {
return isVideo(item) ? 'video/mp4' : 'image/jpeg'
}
const getItemId = (item: InstagramMedia): string => item.id
const getItemRequestPath = (item: InstagramMedia): string => item.id
const getItemModifiedDate = (item: InstagramMedia): string => item.timestamp
const getItemThumbnailUrl = (item: InstagramMedia): string | undefined =>
getItemIcon(item)
const getNextPagePath = (
data: InstagramListResponse,
currentQuery: Record<string, string>,
currentPath: string | undefined,
): string | null => {
if (!data.paging || !data.paging.cursors) {
return null
}
const after = data.paging.cursors.after
if (after == null || after.length === 0) return null
const query = { ...currentQuery, cursor: after }
return `${currentPath || ''}?${querystring.stringify(query)}`
}
const adaptData = (
res: InstagramListResponse,
username: string | null,
directory: string | undefined,
currentQuery: Record<string, string>,
) => {
const items = getItemSubList(res).map((item, i) => ({
isFolder: isFolder(item),
icon: getItemIcon(item),
name: getItemName(item, i),
mimeType: getMimeType(item),
id: getItemId(item),
size: null,
thumbnail: getItemThumbnailUrl(item),
requestPath: getItemRequestPath(item),
modifiedDate: getItemModifiedDate(item),
}))
const nextPagePath = getNextPagePath(res, currentQuery, directory)
return { username, items, nextPagePath }
}
export default adaptData

View file

@ -1,161 +0,0 @@
import type { Readable } from 'node:stream'
import got from 'got'
import { isRecord } from '../../../helpers/type-guards.js'
import { prepareStream } from '../../../helpers/utils.js'
import logger from '../../../logger.js'
import Provider, {
type ProviderListResponse,
type Query,
} from '../../Provider.js'
import { withProviderErrorHandling } from '../../providerErrors.js'
import adaptData from './adapter.js'
type InstagramClient = ReturnType<typeof got.extend>
interface InstagramUserSession {
accessToken: string
}
const getClient = ({ token }: { token: string }): InstagramClient =>
got.extend({
prefixUrl: 'https://graph.instagram.com',
headers: {
authorization: `Bearer ${token}`,
},
})
async function getMediaUrl({
token,
id,
}: {
token: string
id: string
}): Promise<string> {
const body = await getClient({ token })
.get(String(id), {
searchParams: { fields: 'media_url' },
responseType: 'json',
})
.json<{ media_url: string }>()
return body.media_url
}
/**
* Adapter for API https://developers.facebook.com/docs/instagram-api/overview
*/
export default class Instagram extends Provider<InstagramUserSession> {
// for "grant"
static override getExtraGrantConfig() {
return {
protocol: 'https',
scope: ['user_profile', 'user_media'],
}
}
static override get oauthProvider() {
return 'instagram'
}
override async list({
directory,
providerUserSession: { accessToken: token },
query,
}: {
directory?: string | undefined
providerUserSession: InstagramUserSession
query?: Query | undefined
}): Promise<ProviderListResponse> {
return this.#withErrorHandling(
'provider.instagram.list.error',
async () => {
const qs: Record<string, string> = {
fields:
'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}',
}
if (typeof query?.['cursor'] === 'string') {
qs['after'] = query['cursor']
}
const client = getClient({ token })
const [me, list] = await Promise.all([
client
.get('me', {
searchParams: { fields: 'username' },
responseType: 'json',
})
.json<{ username?: string }>(),
client
.get('me/media', { searchParams: qs, responseType: 'json' })
.json<Parameters<typeof adaptData>[0]>(),
])
const username = me.username != null ? me.username : null
const currentQuery: Record<string, string> = {}
if (typeof query?.['cursor'] === 'string')
currentQuery['cursor'] = query['cursor']
return adaptData(list, username, directory, currentQuery)
},
)
}
override async download({
id,
providerUserSession: { accessToken: token },
}: {
id: string
providerUserSession: InstagramUserSession
}): Promise<{ stream: Readable; size: number | undefined }> {
return this.#withErrorHandling(
'provider.instagram.download.error',
async () => {
const url = await getMediaUrl({ token, id })
const stream = got.stream.get(url, { responseType: 'json' })
const { size } = await prepareStream(stream)
return { stream, size }
},
)
}
override async thumbnail(): Promise<{
stream: Readable
contentType: string
}> {
// not implementing this because a public thumbnail from instagram will be used instead
logger.error(
'call to thumbnail is not implemented',
'provider.instagram.thumbnail.error',
)
throw new Error('call to thumbnail is not implemented')
}
override async logout() {
// access revoke is not supported by Instagram's API
return {
revoked: false,
manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/',
}
}
async #withErrorHandling<T>(tag: string, fn: () => Promise<T>): Promise<T> {
return withProviderErrorHandling({
fn,
tag,
providerName: Instagram.oauthProvider,
isAuthError: (response) => {
const body = response.body
if (!isRecord(body)) return false
const err = body['error']
return isRecord(err) && err['code'] === 190
}, // Invalid OAuth 2.0 Access Token
getJsonErrorMessage: (body) => {
if (!isRecord(body)) return undefined
const err = body['error']
if (!isRecord(err)) return undefined
return typeof err['message'] === 'string' ? err['message'] : undefined
},
})
}
}

View file

@ -1,10 +1,10 @@
import assert from 'node:assert'
import type { Server as HttpServer } from 'node:http'
import type { Server as HttpsServer } from 'node:https'
import { WebSocketServer } from 'ws'
import { type WebSocket, WebSocketServer } from 'ws'
import type { CompanionInitOptions } from '../schemas/companion.js'
import emitter from './emitter/index.js'
import { isRecord } from './helpers/type-guards.js'
import { jsonStringify } from './helpers/utils.js'
import { getURLBuilder, jsonStringify } from './helpers/utils.js'
import * as logger from './logger.js'
import * as redis from './redis.js'
import Uploader from './Uploader.js'
@ -19,57 +19,134 @@ function isSocketMessage(value: unknown): value is SocketMessage {
)
}
/**
* The socket is used to send progress events during an upload.
*/
export default function setupSocket(server: HttpServer | HttpsServer): void {
const wss = new WebSocketServer({ server })
const redisClient = redis.client()
function handleUploadSocketConnection({
token,
ws,
}: {
token: string
ws: WebSocket
}) {
// A new connection is usually created when an upload begins,
// or when connection fails while an upload is on-going and,
// client attempts to reconnect.
wss.on('connection', (ws, req) => {
const fullPath = req.url
assert(fullPath != null, 'WebSocket connection URL is missing')
// the token identifies which ongoing upload's progress, the socket
// connection wishes to listen to.
const token = fullPath.replace(/^.*\/api\//, '')
logger.info(`connection received from ${token}`, 'socket.connect')
function send(data: SocketMessage): void {
ws.send(jsonStringify(data), (err) => {
if (err) {
logger.error(err, 'socket.redis.error', Uploader.shortenToken(token))
// the token identifies which ongoing upload's progress, the socket
// connection wishes to listen to.
const redisClient = redis.client()
function send(data: { action: string; payload: object }) {
ws.send(jsonStringify(data), (err) => {
if (err)
logger.error(
err,
'socket.upload.redis.error',
Uploader.shortenToken(token),
)
})
}
// if the redisClient is available, then we attempt to check the storage
// if we have any already stored state on the upload.
if (redisClient) {
redisClient
.get(`${Uploader.STORAGE_PREFIX}:${token}`)
.then((data) => {
if (data) {
const dataObj = JSON.parse(data.toString())
if (isSocketMessage(dataObj)) send(dataObj)
}
})
}
.catch((err) =>
logger.error(
err,
'socket.upload.redis.error',
Uploader.shortenToken(token),
),
)
}
// if the redisClient is available, then we attempt to check the storage
// if we have any already stored state on the upload, and send it to the client immediately after connection,
// so that the client can update the UI accordingly without the user having to wait for another event
if (redisClient) {
redisClient
.get(`${Uploader.STORAGE_PREFIX}:${token}`)
.then((data) => {
if (!data) return
const dataObj: unknown = JSON.parse(data)
if (isSocketMessage(dataObj)) {
send(dataObj)
}
})
.catch((err) =>
logger.error(err, 'socket.redis.error', Uploader.shortenToken(token)),
emitter().emit(`connection:${token}`)
const onRedisMessage = (...args: unknown[]) => {
const data = args[0]
if (!isSocketMessage(data)) return
send(data)
}
emitter().on(token, onRedisMessage)
ws.on('message', (jsonData) => {
try {
const data: unknown = JSON.parse(jsonData.toString())
// whitelist triggered actions
const action = isRecord(data) ? data['action'] : undefined
if (action === 'pause' || action === 'resume' || action === 'cancel') {
emitter().emit(`${action}:${token}`)
}
} catch (err) {
logger.error(err, 'websocket.error', Uploader.shortenToken(token))
}
})
ws.on('close', () => {
emitter().removeListener(token, onRedisMessage)
})
}
function handleAuthCallbackSocketConnection({
token,
ws,
}: {
token: string
ws: WebSocket
}) {
function send(data: unknown) {
ws.send(jsonStringify(data), (err) => {
if (err)
logger.error(
err,
'socket.auth.redis.error',
Uploader.shortenToken(token),
)
})
}
// todo we should use a unique prefix for these and upload tokens, so that we can easily distinguish them in the emitter and avoid any potential conflicts.
// but it's a breaking change so let's not do it now
// it's unlikely there will be any collision
emitter().on(token, send)
ws.on('close', () => {
emitter().removeListener(token, send)
})
}
export default function setupSockets(
server: HttpServer | HttpsServer,
companionOptions: CompanionInitOptions,
) {
const wss = new WebSocketServer({ server })
const urlBuilder = getURLBuilder(companionOptions)
const externalBasePath = urlBuilder('', true, true)
wss.on('connection', (ws, req) => {
// basic router:
let path = req.url
// strip off base path if any
if (path != null && externalBasePath && path.startsWith(externalBasePath)) {
path = path.slice(externalBasePath.length)
}
emitter().emit(`connection:${token}`)
const onTokenMessage = (...args: unknown[]) => {
const data = args[0]
if (!isSocketMessage(data)) return
send(data)
}
emitter().on(token, onTokenMessage)
// authentication callback token?
const authCallbackTokenMatch = path?.match(
/^\/api2\/auth-callback\/token\/([^/]+)/,
)
const authCallbackToken = authCallbackTokenMatch?.[1]
// or token that identifies which ongoing upload's progress, the socket connection wishes to listen to.
const uploadTokenMatch = path?.match(/^\/api\/([^/]+)/)
const uploadToken = uploadTokenMatch?.[1]
ws.on('error', (err) => {
// https://github.com/websockets/ws/issues/1543
@ -81,33 +158,38 @@ export default function setupSocket(server: HttpServer | HttpsServer): void {
) {
logger.error(
'WebSocket message too large',
'websocket.error',
Uploader.shortenToken(token),
'socket.upload.error',
uploadToken && Uploader.shortenToken(uploadToken),
)
} else {
logger.error(err, 'websocket.error', Uploader.shortenToken(token))
}
})
ws.on('message', (jsonData) => {
try {
const data: unknown = JSON.parse(jsonData.toString())
// whitelist triggered actions
const action = isRecord(data) ? data['action'] : undefined
if (action === 'pause' || action === 'resume' || action === 'cancel') {
emitter().emit(`${action}:${token}`)
}
} catch (err) {
logger.error(
err instanceof Error ? err : String(err),
'websocket.error',
Uploader.shortenToken(token),
err,
'socket.error',
uploadToken && Uploader.shortenToken(uploadToken),
)
}
})
ws.on('close', () => {
emitter().removeListener(token, onTokenMessage)
})
if (uploadToken) {
logger.info(
`Upload token connection received from ${uploadToken}`,
'socket.upload.connect',
)
handleUploadSocketConnection({ token: uploadToken, ws })
return
}
if (authCallbackToken) {
logger.info(
`Auth callback token connection received from ${authCallbackToken}`,
'socket.auth.callback.connect',
)
handleAuthCallbackSocketConnection({ token: authCallbackToken, ws })
return
}
ws.close()
})
}

View file

@ -145,11 +145,6 @@ const getConfigFromEnv = (): StandaloneCompanionOptions => {
secret: getSecret('COMPANION_BOX_SECRET'),
credentialsURL: process.env['COMPANION_BOX_KEYS_ENDPOINT'],
},
instagram: {
key: process.env['COMPANION_INSTAGRAM_KEY'],
secret: getSecret('COMPANION_INSTAGRAM_SECRET'),
credentialsURL: process.env['COMPANION_INSTAGRAM_KEYS_ENDPOINT'],
},
facebook: {
key: process.env['COMPANION_FACEBOOK_KEY'],
secret: getSecret('COMPANION_FACEBOOK_SECRET'),

View file

@ -158,7 +158,7 @@ export default function server(inputCompanionOptions?: CompanionInitOptions) {
}
// initialize companion
const { app: companionApp } = companion.app(companionOptions)
const { app: companionApp, emitter } = companion.app(companionOptions)
// add companion to server middleware
router.use(companionApp)
@ -216,5 +216,5 @@ export default function server(inputCompanionOptions?: CompanionInitOptions) {
}
})
return { app, companionOptions }
return { app, companionOptions, emitter }
}

View file

@ -7,9 +7,9 @@ import standalone from './index.js'
const port: string | number =
process.env['COMPANION_PORT'] || process.env['PORT'] || 3020
const { app } = standalone()
const { app, companionOptions } = standalone()
companion.socket(app.listen(port))
companion.socket(app.listen(port), companionOptions)
logger.info(`Welcome to Companion! v${packageJson.version}`)
logger.info(`Listening on http://localhost:${port}`)

View file

@ -1,16 +1,19 @@
import request from 'supertest'
import { describe, expect, test, vi } from 'vitest'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import * as tokenService from '../src/server/helpers/jwt.js'
import mockOauthState from './mockoauthstate.js'
import { getServer, grantToken } from './mockserver.js'
vi.mock('express-prom-bundle')
mockOauthState()
const secret = 'secret'
beforeEach(() => {
vi.mock('express-prom-bundle')
vi.resetModules()
vi.clearAllMocks()
})
describe('test authentication callback', () => {
test('authentication callback redirects to send-token url', async () => {
const { getServer } = await import('./mockserver.js')
return request(await getServer())
.get('/drive/callback')
.expect(302)
@ -22,6 +25,7 @@ describe('test authentication callback', () => {
})
test('authentication callback sets cookie', async () => {
const { getServer, grantToken } = await import('./mockserver.js')
return request(await getServer())
.get('/dropbox/callback')
.expect(302)
@ -45,13 +49,55 @@ describe('test authentication callback', () => {
})
})
test('the token gets sent via html', async () => {
test('the token gets sent via websocket', async () => {
const callbackToken = 'auth-callback-token'
const oauthState = await import('../src/server/helpers/oauth-state.js')
vi.spyOn(oauthState, 'getFromState').mockImplementation((state, key) => {
if (key === 'authCallbackToken') return callbackToken
return 'http://localhost:3020'
})
const authData = {
dropbox: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(authData, secret)
const onEmitted = vi.fn()
const { getServerWithEmitter } = await import('./mockserver.js')
const { server, emitter } = await getServerWithEmitter()
emitter.on(callbackToken, onEmitted)
await request(server)
.get(`/dropbox/send-token?uppyAuthToken=${encodeURIComponent(token)}`)
.expect(200)
.expect((res) => {
expect(res.text).toMatch('Authentication successful')
})
expect(onEmitted).toHaveBeenLastCalledWith({
token,
})
})
test('the token gets sent via legacy html mechanism', async () => {
const oauthState = await import('../src/server/helpers/oauth-state.js')
vi.spyOn(oauthState, 'getFromState').mockImplementation((state, key) => {
if (key === 'authCallbackToken') return undefined
return 'http://localhost:3020'
})
const authData = {
dropbox: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(authData, secret)
const { getServer } = await import('./mockserver.js')
// see mock ../../src/server/helpers/oauth-state above for state values
return request(await getServer())
.get(`/dropbox/send-token?uppyAuthToken=${encodeURIComponent(token)}`)

View file

@ -3,7 +3,6 @@ import * as constants from './constants.js'
import * as drive from './drive.js'
import * as dropbox from './dropbox.js'
import * as facebook from './facebook.js'
import * as instagram from './instagram.js'
import * as onedrive from './onedrive.js'
import * as zoom from './zoom.js'
@ -11,7 +10,6 @@ export const providers = {
box,
drive,
dropbox,
instagram,
onedrive,
facebook,
zoom,

View file

@ -1,9 +0,0 @@
export const expects: {
itemName: string
itemMimeType: string
itemSize: null
} = {
itemName: 'Instagram 2017-08-31T18:10:00+00000.jpeg',
itemMimeType: 'image/jpeg',
itemSize: null,
}

View file

@ -37,11 +37,6 @@ const defaultEnv = {
COMPANION_GOOGLE_KEYS_ENDPOINT: undefined,
COMPANION_GOOGLE_SECRET_FILE: undefined,
COMPANION_INSTAGRAM_KEY: 'instagram_key',
COMPANION_INSTAGRAM_SECRET: 'instagram_secret',
COMPANION_INSTAGRAM_KEYS_ENDPOINT: undefined,
COMPANION_INSTAGRAM_SECRET_FILE: undefined,
COMPANION_FACEBOOK_KEY: 'facebook_key',
COMPANION_FACEBOOK_SECRET: 'facebook_secret',
COMPANION_FACEBOOK_KEYS_ENDPOINT: undefined,
@ -89,9 +84,9 @@ export const grantToken = 'fake token'
// companion stores certain global state, so the user needs to reset modules for each test
// todo rewrite companion to not use global state
// https://github.com/transloadit/uppy/issues/3284
export const getServer = async (
export const getServerWithEmitter = async (
extraEnv: Record<string, string | number | undefined> = {},
): Promise<ReturnType<typeof express>> => {
) => {
const { default: standalone } = await import('../src/standalone/index.js')
const env = {
@ -125,7 +120,14 @@ export const getServer = async (
next()
})
const { app } = standalone()
const { app, emitter } = standalone()
authServer.use(app)
return authServer
return { server: authServer, emitter }
}
export const getServer = async (
extraEnv?: Record<string, string | number | undefined> | undefined,
) => {
const { server } = await getServerWithEmitter(extraEnv)
return server
}

View file

@ -74,32 +74,17 @@ describe('Test Provider options', () => {
expect(googledrive['secret']).toBe('google_secret')
const instagram = requireGrantProviderConfig(grantConfig, 'instagram')
expect(instagram['key']).toBe('instagram_key')
expect(instagram['secret']).toBe('instagram_secret')
const zoom = requireGrantProviderConfig(grantConfig, 'zoom')
expect(zoom['key']).toBe('zoom_key')
expect(zoom['secret']).toBe('zoom_secret')
})
test('adds extra provider config', () => {
process.env['COMPANION_INSTAGRAM_KEY'] = '123456'
providerManager.addProviderOptions(
getAddProviderOptionsArgs(getCompanionOptions()),
grantConfig,
getOauthProvider,
)
expect(requireGrantProviderConfig(grantConfig, 'instagram')).toEqual({
transport: 'session',
state: true,
callback: '/instagram/callback',
redirect_uri: 'http://localhost:3020/instagram/redirect',
key: '123456',
secret: 'instagram_secret',
protocol: 'https',
scope: ['user_profile', 'user_media'],
})
expect(requireGrantProviderConfig(grantConfig, 'dropbox')).toEqual({
key: 'dropbox_key',
@ -164,8 +149,6 @@ describe('Test Provider options', () => {
`${process.env['PWD']}/test/resources/box_secret_file`
process.env['COMPANION_GOOGLE_SECRET_FILE'] =
`${process.env['PWD']}/test/resources/google_secret_file`
process.env['COMPANION_INSTAGRAM_SECRET_FILE'] =
`${process.env['PWD']}/test/resources/instagram_secret_file`
process.env['COMPANION_ZOOM_SECRET_FILE'] =
`${process.env['PWD']}/test/resources/zoom_secret_file`
process.env['COMPANION_ZOOM_VERIFICATION_TOKEN_FILE'] =
@ -188,9 +171,6 @@ describe('Test Provider options', () => {
expect(
requireGrantProviderConfig(grantConfig, 'googledrive')['secret'],
).toBe('elgoog')
expect(requireGrantProviderConfig(grantConfig, 'instagram')['secret']).toBe(
'margatsni',
)
expect(requireGrantProviderConfig(grantConfig, 'zoom')['secret']).toBe(
'u8Z5ceq',
)
@ -232,13 +212,6 @@ describe('Test Provider options', () => {
getGrantProviderField(grantConfig, 'googledrive', 'secret'),
).toBeUndefined()
expect(
getGrantProviderField(grantConfig, 'instagram', 'key'),
).toBeUndefined()
expect(
getGrantProviderField(grantConfig, 'instagram', 'secret'),
).toBeUndefined()
expect(getGrantProviderField(grantConfig, 'zoom', 'key')).toBeUndefined()
expect(getGrantProviderField(grantConfig, 'zoom', 'secret')).toBeUndefined()
})
@ -262,9 +235,6 @@ describe('Test Provider options', () => {
expect(
requireGrantProviderConfig(grantConfig, 'googledrive')['redirect_uri'],
).toBe('http://domain.com/drive/redirect')
expect(
requireGrantProviderConfig(grantConfig, 'instagram')['redirect_uri'],
).toBe('http://domain.com/instagram/redirect')
expect(
requireGrantProviderConfig(grantConfig, 'zoom')['redirect_uri'],
).toBe('http://domain.com/zoom/redirect')

View file

@ -316,30 +316,6 @@ describe('list provider files', () => {
expect1({ username, items, providerFixture })
})
test('instagram', async () => {
nock('https://graph.instagram.com').get('/me?fields=username').reply(200, {
id: '17841405793187218',
username: defaults.USERNAME,
})
nock('https://graph.instagram.com')
.get(
'/me/media?fields=id%2Cmedia_type%2Cthumbnail_url%2Cmedia_url%2Ctimestamp%2Cchildren%7Bmedia_type%2Cmedia_url%2Cthumbnail_url%2Ctimestamp%7D',
)
.reply(200, {
data: [
{
id: defaults.ITEM_ID,
media_type: 'IMAGE',
timestamp: '2017-08-31T18:10:00+0000',
media_url: defaults.THUMBNAIL_URL,
},
],
})
const { username, items, providerFixture } = await runTest('instagram')
expect1({ username, items, providerFixture })
})
test('onedrive', async () => {
nock('https://graph.microsoft.com').get('/v1.0/me').reply(200, {
userPrincipalName: defaults.USERNAME,
@ -490,20 +466,6 @@ describe('provider file gets downloaded from', () => {
await runTest('facebook')
})
test('instagram', async () => {
// times(2) because of size request
nock('https://graph.instagram.com')
.get(`/${defaults.ITEM_ID}?fields=media_url`)
.times(2)
.reply(200, {
id: defaults.ITEM_ID,
media_type: 'IMAGE',
media_url: defaults.THUMBNAIL_URL,
timestamp: '2017-08-31T18:10:00+0000',
})
await runTest('instagram')
})
test('onedrive', async () => {
nock('https://graph.microsoft.com')
.get(`/v1.0/drives/DUMMY-DRIVE-ID/items/${defaults.ITEM_ID}`)
@ -609,10 +571,6 @@ describe('logout of provider', () => {
await runTest('facebook')
})
test('instagram', async () => {
await runTest('instagram')
})
test('onedrive', async () => {
await runTest('onedrive')
})

View file

@ -8,7 +8,6 @@ export type ProviderIconProps = {
| 'audio'
| 'dropbox'
| 'facebook'
| 'instagram'
| 'onedrive'
| 'googlephotos'
| 'googledrive'
@ -94,36 +93,6 @@ export default function ProviderIcon(props: ProviderIconProps) {
</g>
</svg>
)
case 'instagram':
return (
<svg
aria-hidden="true"
focusable="false"
width="32"
height="32"
viewBox="0 0 32 32"
>
<defs>
<path
d="M16.825 5l.483-.001.799.002c1.168.005 1.598.021 2.407.057 1.17.05 1.97.235 2.67.506.725.28 1.34.655 1.951 1.265.613.61.99 1.223 1.273 1.946.273.7.46 1.498.516 2.67l.025.552.008.205c.029.748.037 1.51.042 3.777l.001.846v.703l-.001.398a50.82 50.82 0 01-.058 2.588c-.05 1.17-.235 1.97-.506 2.67a5.394 5.394 0 01-1.265 1.951c-.61.613-1.222.99-1.946 1.273-.699.273-1.498.46-2.668.516-.243.012-.451.022-.656.03l-.204.007c-.719.026-1.512.034-3.676.038l-.847.001h-1.1a50.279 50.279 0 01-2.587-.059c-1.171-.05-1.971-.235-2.671-.506a5.394 5.394 0 01-1.951-1.265 5.385 5.385 0 01-1.272-1.946c-.274-.699-.46-1.498-.517-2.668a88.15 88.15 0 01-.03-.656l-.007-.205c-.026-.718-.034-1.512-.038-3.674v-2.129c.006-1.168.022-1.597.058-2.406.051-1.171.235-1.971.506-2.672a5.39 5.39 0 011.265-1.95 5.381 5.381 0 011.946-1.272c.699-.274 1.498-.462 2.669-.517l.656-.03.204-.007c.718-.026 1.511-.034 3.674-.038zm.678 1.981h-1.226l-.295.001c-2.307.005-3.016.013-3.777.043l-.21.009-.457.02c-1.072.052-1.654.232-2.042.383-.513.2-.879.44-1.263.825a3.413 3.413 0 00-.82 1.267c-.15.388-.33.97-.375 2.043a48.89 48.89 0 00-.056 2.482v.398 1.565c.006 2.937.018 3.285.073 4.444.05 1.073.231 1.654.382 2.043.2.512.44.878.825 1.263.386.383.753.621 1.267.82.388.15.97.328 2.043.374.207.01.388.017.563.024l.208.007a63.28 63.28 0 002.109.026h1.564c2.938-.006 3.286-.019 4.446-.073 1.071-.051 1.654-.232 2.04-.383.514-.2.88-.44 1.264-.825.384-.386.622-.753.82-1.266.15-.389.328-.971.375-2.044.039-.88.054-1.292.057-2.723v-1.15-.572c-.006-2.936-.019-3.284-.074-4.445-.05-1.071-.23-1.654-.382-2.04-.2-.515-.44-.88-.825-1.264a3.405 3.405 0 00-1.267-.82c-.388-.15-.97-.328-2.042-.375a48.987 48.987 0 00-2.535-.056zm-1.515 3.37a5.65 5.65 0 11.021 11.299 5.65 5.65 0 01-.02-11.3zm.004 1.982a3.667 3.667 0 10.015 7.334 3.667 3.667 0 00-.015-7.334zm5.865-3.536a1.32 1.32 0 11.005 2.64 1.32 1.32 0 01-.005-2.64z"
id="a"
/>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="b" fill="#fff">
<use xlinkHref="#a" />
</mask>
<image
mask="url(#b)"
x="4"
y="4"
width="24"
height="24"
xlinkHref="data:image/png;base64,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"
/>
</g>
</svg>
)
case 'onedrive':
return (
<svg

View file

@ -14,7 +14,6 @@ export type RemoteSourceKeys =
| 'Dropbox'
| 'Facebook'
| 'GoogleDrive'
| 'Instagram'
| 'OneDrive'
| 'Unsplash'
| 'Url'

View file

@ -11,8 +11,8 @@ Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly
with any application. Its fast, provides a comprehensible API and lets you
worry about more important problems than building a file uploader.
- **Fetch** files from local disk, remote urls, Google Drive, Dropbox,
Instagram, or snap and record selfies with a camera;
- **Fetch** files from local disk, remote urls, Google Drive, Dropbox, or snap
and record selfies with a camera;
- **Preview** and edit metadata with a nice interface;
- **Upload** to the final destination, optionally process/encode

View file

@ -1,6 +1,6 @@
{
"name": "@uppy/core",
"description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
"description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox, Google Drive, S3 and more :dog:",
"version": "5.2.0",
"license": "MIT",
"style": "dist/style.min.css",

View file

@ -1962,7 +1962,7 @@ describe('src/Core', () => {
})
core.addFile({
source: 'instagram',
source: 'dropbox',
name: 'foo.jpg',
type: 'image/jpeg',
// @ts-ignore
@ -2028,7 +2028,7 @@ describe('src/Core', () => {
})
})
core.addFile({
source: 'instagram',
source: 'dropbox',
name: 'foo.jpg',
type: 'image/jpeg',
// @ts-ignore
@ -2045,7 +2045,7 @@ describe('src/Core', () => {
})
})
core.addFile({
source: 'instagram',
source: 'dropbox',
name: 'bar.jpg',
type: 'image/jpeg',
// @ts-ignore

View file

@ -10,8 +10,8 @@
Dashboard is a universal UI plugin for Uppy:
- Drag and Drop, paste, select from local disk / my device
- UI for Webcam and remote sources: Google Drive, Dropbox, Instagram (all
optional, added via plugins)
- UI for Webcam and remote sources: Google Drive, Dropbox (all optional, added
via plugins)
- File previews and info
- Metadata editor
- Progress: total and for individual files

View file

@ -24,8 +24,6 @@ function focusOnLastNode(event: $TSFixMe, nodes: $TSFixMe) {
// Firefox thinks <ul> is focusable, but we don't have <ul>s in our FOCUSABLE_ELEMENTS. Which means that if we tab into
// the <ul>, code will think that we are not in the active overlay, and we should focusOnFirstNode() of the currently
// active overlay!
// [Practical check] if we use (focusedItemIndex === -1), instagram provider in firefox will never get focus on its pics
// in the <ul>.
function isFocusInOverlay(activeOverlayEl: $TSFixMe) {
return activeOverlayEl.contains(document.activeElement)
}
@ -60,7 +58,7 @@ function trapFocus(
}
}
// Traps focus inside of the currently open overlay (e.g. Dashboard, or e.g. Instagram),
// Traps focus inside of the currently open overlay (e.g. Dashboard),
// never lets focus disappear from the modal.
export { trapFocus as forModal }

View file

@ -1 +0,0 @@
tsconfig.*

View file

@ -1,183 +0,0 @@
# @uppy/instagram
## 5.1.0
### Minor Changes
- 79e6460: - Add PluginTypeRegistry and typed getPlugin overload in @uppy/core
- Register plugin ids across packages so uppy.getPlugin('Dashboard' | 'Webcam') returns the concrete plugin type and removes the need to pass generics in getPlugin()
### Patch Changes
- Updated dependencies [e661348]
- Updated dependencies [79e6460]
- Updated dependencies [ac12f35]
- Updated dependencies [4817585]
- @uppy/provider-views@5.2.0
- @uppy/core@5.2.0
- @uppy/utils@7.1.4
## 5.0.1
### Patch Changes
- 975317d: Removed "main" from package.json, since export maps serve as the contract for the public API.
- Updated dependencies [4b6a76c]
- Updated dependencies [975317d]
- Updated dependencies [9bac4c8]
- @uppy/core@5.0.2
- @uppy/companion-client@5.0.1
- @uppy/provider-views@5.0.2
- @uppy/utils@7.0.2
## 5.0.0
### Major Changes
- c5b51f6: ### Export maps for all packages
All packages now have export maps. This is a breaking change in two cases:
1. The css imports have changed from `@uppy[package]/dist/styles.min.css` to `@uppy[package]/css/styles.min.css`
2. You were importing something that wasn't exported from the root, for instance `@uppy/core/lib/foo.js`. You can now only import things we explicitly exported.
#### Changed imports for `@uppy/react`, `@uppy/vue`, and `@uppy/svelte`
Some components, like Dashboard, require a peer dependency to work but since all components were exported from a single file you were forced to install all peer dependencies. Even if you never imported, for instance, the status bar component.
Every component that requires a peer dependency has now been moved to a subpath, such as `@uppy/react/dashboard`, so you only need to install the peer dependencies you need.
**Example for `@uppy/react`:**
**Before:**
```javascript
import { Dashboard, StatusBar } from "@uppy/react";
```
**Now:**
```javascript
import Dashboard from "@uppy/react/dashboard";
import StatusBar from "@uppy/react/status-bar";
```
### Patch Changes
- Updated dependencies [d301c01]
- Updated dependencies [c5b51f6]
- @uppy/utils@7.0.0
- @uppy/companion-client@5.0.0
- @uppy/provider-views@5.0.0
- @uppy/core@5.0.0
## 4.3.2
### Patch Changes
- 1b1a9e3: Define "files" in package.json
- Updated dependencies [1b1a9e3]
- Updated dependencies [c66fd85]
- @uppy/companion-client@4.5.2
- @uppy/provider-views@4.5.2
- @uppy/utils@6.2.2
- @uppy/core@4.5.2
## 4.3.0
### Minor Changes
- 0c24c5a: Use TypeScript compiler instead of Babel
### Patch Changes
- Updated dependencies [0c24c5a]
- Updated dependencies [0c24c5a]
- @uppy/core@4.5.0
- @uppy/companion-client@4.5.0
- @uppy/provider-views@4.5.0
- @uppy/utils@6.2.0
## 4.2.3
Released: 2025-05-18
Included in: Uppy v4.16.0
- @uppy/audio,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/image-editor,@uppy/instagram,@uppy/onedrive,@uppy/remote-sources,@uppy/screen-capture,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/webcam,@uppy/webdav,@uppy/zoom: ts: make locale strings optional (Merlijn Vos / #5728)
## 4.2.0
Released: 2025-01-06
Included in: Uppy v4.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/webdav,@uppy/xhr-upload,@uppy/zoom: Remove "paths" from all tsconfig's (Merlijn Vos / #5572)
## 4.1.2
Released: 2024-12-05
Included in: Uppy v4.8.0
- @uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: cleanup tsconfig (Mikael Finstad / #5520)
## 4.1.1
Released: 2024-10-31
Included in: Uppy v4.6.0
- @uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react-native,@uppy/react,@uppy/redux-dev-tools,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Fix links (Anthony Veaudry / #5492)
## 4.1.0
Released: 2024-08-29
Included in: Uppy v4.3.0
- @uppy/aws-s3,@uppy/box,@uppy/compressor,@uppy/dropbox,@uppy/facebook,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/screen-capture,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/xhr-upload,@uppy/zoom: export plugin options (Antoine du Hamel / #5433)
## 4.0.0-beta.1
Released: 2024-03-28
Included in: Uppy v4.0.0-beta.1
- @uppy/instagram: refactor to TypeScript (Murderlon / #4979)
## 3.3.0
Released: 2024-03-27
Included in: Uppy v3.24.0
- @uppy/instagram: refactor to TypeScript (Murderlon / #4979)
## 3.0.1
Released: 2022-09-25
Included in: Uppy v3.1.0
- @uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/companion,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/redux-dev-tools,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092)
## 3.0.0
Released: 2022-08-22
Included in: Uppy v3.0.0
- Switch to ESM
## 2.1.1
Released: 2022-05-30
Included in: Uppy v2.11.0
- @uppy/angular,@uppy/audio,@uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/onedrive,@uppy/progress-bar,@uppy/react,@uppy/redux-dev-tools,@uppy/robodog,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763)
## 2.1.0
Released: 2022-05-14
Included in: Uppy v2.10.0
- @uppy/instagram: refactor to ESM (Antoine du Hamel / #3696)
## 2.0.5
Released: 2021-12-07
Included in: Uppy v2.3.0
- @uppy/aws-s3,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive,@uppy/image-editor,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/url,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Refactor locale scripts & generate types and docs (Merlijn Vos / #3276)

View file

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018 Transloadit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,48 +0,0 @@
# @uppy/instagram
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">
[![npm version](https://img.shields.io/npm/v/@uppy/instagram.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/instagram)
![CI status for Uppy tests](https://github.com/transloadit/uppy/workflows/CI/badge.svg)
![CI status for Companion tests](https://github.com/transloadit/uppy/workflows/Companion/badge.svg)
![CI status for browser tests](https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg)
The Instagram plugin lets users import photos from their Instagram account.
A [Companion](https://uppy.io/docs/companion) instance is required for the
Instagram plugin to work. Companion handles authentication with Instagram,
downloads the pictures and videos, and uploads them to the destination. This
saves the user bandwidth, especially helpful if they are on a mobile connection.
Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
a versatile file encoding service.
## Example
```js
import Uppy from '@uppy/core'
import Instagram from '@uppy/instagram'
const uppy = new Uppy()
uppy.use(Instagram, {})
```
## Installation
```bash
$ npm install @uppy/instagram
```
Alternatively, you can also use this plugin in a pre-built bundle from
Transloadits CDN: Smart CDN. In that case `Uppy` will attach itself to the
global `window.Uppy` object. See the
[main Uppy documentation](https://uppy.io/docs/#Installation) for instructions.
## Documentation
Documentation for this plugin can be found on the
[Uppy website](https://uppy.io/docs/instagram).
## License
[The MIT License](./LICENSE).

View file

@ -1,52 +0,0 @@
{
"name": "@uppy/instagram",
"description": "Import photos and videos from Instagram, into Uppy.",
"version": "5.1.0",
"license": "MIT",
"type": "module",
"sideEffects": false,
"scripts": {
"build": "tsc --build tsconfig.build.json",
"typecheck": "tsc --build"
},
"keywords": [
"file uploader",
"uppy",
"uppy-plugin",
"instagram",
"provider",
"photos",
"videos"
],
"homepage": "https://uppy.io",
"bugs": {
"url": "https://github.com/transloadit/uppy/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
},
"files": [
"src",
"lib",
"dist",
"CHANGELOG.md"
],
"exports": {
".": "./lib/index.js",
"./package.json": "./package.json"
},
"dependencies": {
"@uppy/companion-client": "workspace:^",
"@uppy/provider-views": "workspace:^",
"@uppy/utils": "workspace:^",
"preact": "^10.26.10"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
},
"devDependencies": {
"@uppy/core": "workspace:^",
"typescript": "^5.8.3"
}
}

View file

@ -1,133 +0,0 @@
import {
type CompanionPluginOptions,
getAllowedHosts,
Provider,
tokenStorage,
} from '@uppy/companion-client'
import type {
AsyncStore,
Body,
Meta,
UnknownProviderPlugin,
UnknownProviderPluginState,
UppyFile,
} from '@uppy/core'
import { UIPlugin, type Uppy } from '@uppy/core'
import { ProviderViews } from '@uppy/provider-views'
import type { LocaleStrings } from '@uppy/utils'
// biome-ignore lint/style/useImportType: h is not a type
import { type ComponentChild, h } from 'preact'
import packageJson from '../package.json' with { type: 'json' }
import locale from './locale.js'
declare module '@uppy/core' {
export interface PluginTypeRegistry<M extends Meta, B extends Body> {
Instagram: Instagram<M, B>
}
}
export type InstagramOptions = CompanionPluginOptions & {
locale?: LocaleStrings<typeof locale>
}
export default class Instagram<M extends Meta, B extends Body>
extends UIPlugin<InstagramOptions, M, B, UnknownProviderPluginState>
implements UnknownProviderPlugin<M, B>
{
static VERSION = packageJson.version
icon: () => h.JSX.Element
provider: Provider<M, B>
view!: ProviderViews<M, B>
storage: AsyncStore
files: UppyFile<M, B>[]
rootFolderId: string | null = 'recent'
constructor(uppy: Uppy<M, B>, opts: InstagramOptions) {
super(uppy, opts)
this.type = 'acquirer'
this.files = []
this.storage = this.opts.storage || tokenStorage
this.id = this.opts.id || 'Instagram'
this.icon = () => (
<svg
aria-hidden="true"
focusable="false"
width="32"
height="32"
viewBox="0 0 32 32"
>
<defs>
<path
d="M16.825 5l.483-.001.799.002c1.168.005 1.598.021 2.407.057 1.17.05 1.97.235 2.67.506.725.28 1.34.655 1.951 1.265.613.61.99 1.223 1.273 1.946.273.7.46 1.498.516 2.67l.025.552.008.205c.029.748.037 1.51.042 3.777l.001.846v.703l-.001.398a50.82 50.82 0 01-.058 2.588c-.05 1.17-.235 1.97-.506 2.67a5.394 5.394 0 01-1.265 1.951c-.61.613-1.222.99-1.946 1.273-.699.273-1.498.46-2.668.516-.243.012-.451.022-.656.03l-.204.007c-.719.026-1.512.034-3.676.038l-.847.001h-1.1a50.279 50.279 0 01-2.587-.059c-1.171-.05-1.971-.235-2.671-.506a5.394 5.394 0 01-1.951-1.265 5.385 5.385 0 01-1.272-1.946c-.274-.699-.46-1.498-.517-2.668a88.15 88.15 0 01-.03-.656l-.007-.205c-.026-.718-.034-1.512-.038-3.674v-2.129c.006-1.168.022-1.597.058-2.406.051-1.171.235-1.971.506-2.672a5.39 5.39 0 011.265-1.95 5.381 5.381 0 011.946-1.272c.699-.274 1.498-.462 2.669-.517l.656-.03.204-.007c.718-.026 1.511-.034 3.674-.038zm.678 1.981h-1.226l-.295.001c-2.307.005-3.016.013-3.777.043l-.21.009-.457.02c-1.072.052-1.654.232-2.042.383-.513.2-.879.44-1.263.825a3.413 3.413 0 00-.82 1.267c-.15.388-.33.97-.375 2.043a48.89 48.89 0 00-.056 2.482v.398 1.565c.006 2.937.018 3.285.073 4.444.05 1.073.231 1.654.382 2.043.2.512.44.878.825 1.263.386.383.753.621 1.267.82.388.15.97.328 2.043.374.207.01.388.017.563.024l.208.007a63.28 63.28 0 002.109.026h1.564c2.938-.006 3.286-.019 4.446-.073 1.071-.051 1.654-.232 2.04-.383.514-.2.88-.44 1.264-.825.384-.386.622-.753.82-1.266.15-.389.328-.971.375-2.044.039-.88.054-1.292.057-2.723v-1.15-.572c-.006-2.936-.019-3.284-.074-4.445-.05-1.071-.23-1.654-.382-2.04-.2-.515-.44-.88-.825-1.264a3.405 3.405 0 00-1.267-.82c-.388-.15-.97-.328-2.042-.375a48.987 48.987 0 00-2.535-.056zm-1.515 3.37a5.65 5.65 0 11.021 11.299 5.65 5.65 0 01-.02-11.3zm.004 1.982a3.667 3.667 0 10.015 7.334 3.667 3.667 0 00-.015-7.334zm5.865-3.536a1.32 1.32 0 11.005 2.64 1.32 1.32 0 01-.005-2.64z"
id="a"
/>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="b" fill="#fff">
<use xlinkHref="#a" />
</mask>
<image
mask="url(#b)"
x="4"
y="4"
width="24"
height="24"
xlinkHref="data:image/png;base64,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"
/>
</g>
</svg>
)
this.defaultLocale = locale
this.i18nInit()
this.title = this.i18n('pluginNameInstagram')
this.opts.companionAllowedHosts = getAllowedHosts(
this.opts.companionAllowedHosts,
this.opts.companionUrl,
)
this.provider = new Provider(uppy, {
companionUrl: this.opts.companionUrl,
companionHeaders: this.opts.companionHeaders,
companionKeysParams: this.opts.companionKeysParams,
companionCookiesRule: this.opts.companionCookiesRule,
provider: 'instagram',
pluginId: this.id,
supportsRefreshToken: false,
})
this.render = this.render.bind(this)
}
install(): void {
this.view = new ProviderViews(this, {
provider: this.provider,
viewType: 'grid',
showTitles: false,
showFilter: false,
showBreadcrumbs: false,
})
const { target } = this.opts
if (target) {
this.mount(target, this)
}
}
uninstall(): void {
this.view.tearDown()
this.unmount()
}
render(state: unknown): ComponentChild {
return this.view.render(state)
}
}

View file

@ -1,2 +0,0 @@
export type { InstagramOptions } from './Instagram.js'
export { default } from './Instagram.js'

View file

@ -1,5 +0,0 @@
export default {
strings: {
pluginNameInstagram: 'Instagram',
},
}

View file

@ -1,23 +0,0 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"outDir": "./lib",
"rootDir": "./src"
},
"include": ["./src/**/*.*"],
"exclude": ["./src/**/*.test.ts"],
"references": [
{
"path": "../companion-client/tsconfig.build.json"
},
{
"path": "../provider-views/tsconfig.build.json"
},
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -1,22 +0,0 @@
{
"extends": "../../../tsconfig.shared",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true
},
"include": ["./package.json", "./src/**/*.*"],
"references": [
{
"path": "../companion-client/tsconfig.build.json"
},
{
"path": "../provider-views/tsconfig.build.json"
},
{
"path": "../utils/tsconfig.build.json"
},
{
"path": "../core/tsconfig.build.json"
}
]
}

View file

@ -1,8 +0,0 @@
{
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["^build", "@uppy/core#build"]
}
}
}

View file

@ -134,7 +134,6 @@ ca_ES.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: 'Desenvolupat per %{uppy}',

View file

@ -124,7 +124,6 @@ de_DE.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: 'Powered by %{uppy}',

View file

@ -153,7 +153,6 @@ en_US.strings = {
pluginNameGoogleDrive: 'Google Drive',
pluginNameGoogleDrivePicker: 'Google Drive',
pluginNameGooglePhotosPicker: 'Google Photos',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Screencast',
pluginNameUnsplash: 'Unsplash',

View file

@ -137,7 +137,6 @@ es_ES.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: 'Desarrollado por %{uppy}',

View file

@ -128,7 +128,6 @@ fa_IR.strings = {
pluginNameDropbox: 'دراپ باکس',
pluginNameFacebook: 'فیس بوک',
pluginNameGoogleDrive: 'درایو گوگل',
pluginNameInstagram: 'اینستاگرام',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'بزرگ‌نمایی',
poweredBy: 'قدرت گرفته از %{uppy}',

View file

@ -136,7 +136,6 @@ ms_MY.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Screencast',
pluginNameUnsplash: 'Unsplash',

View file

@ -147,7 +147,6 @@ nl_NL.strings = {
pluginNameGoogleDrive: 'Google Drive',
pluginNameGoogleDrivePicker: 'Google Drive',
pluginNameGooglePhotosPicker: 'Google Photos',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Screencast',
pluginNameUnsplash: 'Unsplash',

View file

@ -144,7 +144,6 @@ pl_PL.strings = {
pluginNameGoogleDrive: 'Google Drive',
pluginNameGoogleDrivePicker: 'Google Drive',
pluginNameGooglePhotosPicker: 'Google Photos',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Zrzut ekranu',
pluginNameUnsplash: 'Unsplash',

View file

@ -148,7 +148,6 @@ pt_BR.strings = {
pluginNameGoogleDrive: 'Google Drive',
pluginNameGoogleDrivePicker: 'Google Drive',
pluginNameGooglePhotosPicker: 'Google Photos',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Screencast',
pluginNameUnsplash: 'Unsplash',

View file

@ -144,7 +144,6 @@ sv_SE.strings = {
pluginNameGoogleDrive: 'Google Drive',
pluginNameGoogleDrivePicker: 'Google Drive',
pluginNameGooglePhotosPicker: 'Google Foton',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameScreenCapture: 'Skärminspelning',
pluginNameUnsplash: 'Unsplash',

View file

@ -138,7 +138,6 @@ uk_UA.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: 'Працює на %{uppy}',

View file

@ -131,7 +131,6 @@ uz_UZ.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: '%{uppy} tomonidan quvvatlanadi',

View file

@ -131,7 +131,6 @@ vi_VN.strings = {
pluginNameDropbox: 'Dropbox',
pluginNameFacebook: 'Facebook',
pluginNameGoogleDrive: 'Google Drive',
pluginNameInstagram: 'Instagram',
pluginNameOneDrive: 'OneDrive',
pluginNameZoom: 'Zoom',
poweredBy: 'Được cung cấp bởi %{uppy}',

View file

@ -24,8 +24,7 @@
"@uppy/dropbox#build",
"@uppy/webdav#build",
"@uppy/unsplash#build",
"@uppy/webcam#build",
"@uppy/instagram#build"
"@uppy/webcam#build"
],
"inputs": [
"script/**/*.{js,ts,jsx,tsx}",

View file

@ -66,7 +66,7 @@ const defaultOptions = {
/**
* SearchProviderView, used for Unsplash and future image search providers.
* Extends generic View, shared with regular providers like Google Drive and Instagram.
* Extends generic View, shared with regular providers like Google Drive.
*/
export default class SearchProviderView<M extends Meta, B extends Body> {
static VERSION = packageJson.version

View file

@ -1,6 +1,6 @@
{
"name": "@uppy/remote-sources",
"description": "Uppy plugin that includes all remote sources that Uppy+Companion offer, like Instagram, Google Drive, Dropox, Box, Unsplash, Url etc",
"description": "Uppy plugin that includes all remote sources that Uppy+Companion offer, like Google Drive, Dropbox, Box, Unsplash, Url etc",
"version": "3.1.0",
"license": "MIT",
"type": "module",
@ -12,7 +12,6 @@
},
"keywords": [
"file uploader",
"instagram",
"google-drive",
"facebook",
"dropbox",
@ -46,7 +45,6 @@
"@uppy/dropbox": "workspace:^",
"@uppy/facebook": "workspace:^",
"@uppy/google-drive": "workspace:^",
"@uppy/instagram": "workspace:^",
"@uppy/onedrive": "workspace:^",
"@uppy/unsplash": "workspace:^",
"@uppy/url": "workspace:^",

View file

@ -46,7 +46,7 @@ describe('RemoteSources', () => {
sources: ['Webcam'],
})
}).toThrow(
'Invalid plugin: "Webcam" is not one of: Box, Dropbox, Facebook, GoogleDrive, Instagram, OneDrive, Unsplash, Url, or Zoom.',
'Invalid plugin: "Webcam" is not one of: Box, Dropbox, Facebook, GoogleDrive, OneDrive, Unsplash, Url, or Zoom.',
)
})

View file

@ -11,7 +11,6 @@ import { BasePlugin } from '@uppy/core'
import Dropbox from '@uppy/dropbox'
import Facebook from '@uppy/facebook'
import GoogleDrive from '@uppy/google-drive'
import Instagram from '@uppy/instagram'
import OneDrive from '@uppy/onedrive'
import Unsplash from '@uppy/unsplash'
import Url from '@uppy/url'
@ -25,7 +24,6 @@ export const availablePlugins = {
Dropbox,
Facebook,
GoogleDrive,
Instagram,
OneDrive,
Unsplash,
Url,
@ -37,7 +35,6 @@ export type AvailablePluginsKeys =
| 'Dropbox'
| 'Facebook'
| 'GoogleDrive'
| 'Instagram'
| 'OneDrive'
| 'Unsplash'
| 'Url'

View file

@ -22,9 +22,6 @@
{
"path": "../google-drive/tsconfig.build.json"
},
{
"path": "../instagram/tsconfig.build.json"
},
{
"path": "../onedrive/tsconfig.build.json"
},

View file

@ -21,9 +21,6 @@
{
"path": "../google-drive/tsconfig.build.json"
},
{
"path": "../instagram/tsconfig.build.json"
},
{
"path": "../onedrive/tsconfig.build.json"
},

View file

@ -140,6 +140,13 @@ class TransloaditAssembly extends Emitter {
this.#sse.addEventListener('assembly_execution_progress', (e) => {
const details = JSON.parse(e.data)
// setting combined execution progress of the assembly
if (typeof details.progress_combined === 'number') {
this.status = {
...this.status,
progress_combined: details.progress_combined,
}
}
this.emit('execution-progress', details)
})

View file

@ -189,6 +189,15 @@ describe('Transloadit', () => {
},
})
// Plugin state should start empty; track every distinct `ok` that lands in
// it so we can verify the assembly lifecycle is reflected.
expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined()
const okHistory = []
const unsubscribe = uppy.store.subscribe((_prev, next) => {
const ok = next.plugins.Transloadit.assemblyStatus?.ok
if (ok && ok !== okHistory.at(-1)) okHistory.push(ok)
})
uppy.addFile({
source: 'test',
name: 'cat.jpg',
@ -228,12 +237,22 @@ describe('Transloadit', () => {
uppy.resumeAll()
await uploadPromise
unsubscribe()
expect(successSpy).toHaveBeenCalled()
// Should be reset to true after upload completes
expect(uppy.getState().allowNewUpload).toBe(true)
// The createAssembly mock returned ASSEMBLY_EXECUTING and the assembly
// setter forwarded that status into plugin state during the upload.
expect(okHistory).toContain('ASSEMBLY_EXECUTING')
// `assemblyStatus` is the live slot — it clears when `this.assembly`
// becomes undefined at the end of `#afterUpload`. `lastAssembly` is
// intentionally not populated here because no terminal event fires in
// this mocked flow (the server keeps returning ASSEMBLY_EXECUTING).
expect(uppy.getState().plugins.Transloadit.assemblyStatus).toBeUndefined()
server.close()
})

View file

@ -26,7 +26,9 @@ import AssemblyWatcher from './AssemblyWatcher.js'
import Client, { type AssemblyError } from './Client.js'
import locale from './locale.js'
export type AssemblyResponse = AssemblyStatus
export type AssemblyResponse = AssemblyStatus & {
progress_combined?: number
}
export type AssemblyFile = AssemblyStatusUpload
export type AssemblyResult = AssemblyStatusResult & { localId: string | null }
export type AssemblyParameters = AssemblyInstructionsInput
@ -79,6 +81,19 @@ type TransloaditState = {
string,
{ assembly: string; id: string; uploadedFile: AssemblyFile }
>
/**
* Live status of the currently-active assembly. Tracks every status
* transition (UPLOADING EXECUTING ...). Cleared automatically when
* `this.assembly = undefined` (no live assembly).
*/
assemblyStatus: AssemblyResponse | undefined
/**
* Snapshot of the most recent non-null status seen. Persists across the
* gap between uploads so the UI can keep showing "your last upload's
* result" after `assemblyStatus` clears. Overwritten by the next
* status update routed through `#handleAssemblyStatusUpdate`.
*/
lastAssemblyStatus: AssemblyResponse | undefined
results: Array<{
result: AssemblyResult
stepName: string
@ -306,7 +321,6 @@ export default class Transloadit<
addPluginVersion('GoogleDrive', 'uppy-google-drive')
addPluginVersion('GoogleDrivePicker', 'uppy-google-drive-picker')
addPluginVersion('GooglePhotosPicker', 'uppy-google-photos-picker')
addPluginVersion('Instagram', 'uppy-instagram')
addPluginVersion('OneDrive', 'uppy-onedrive')
addPluginVersion('Zoom', 'uppy-zoom')
addPluginVersion('Url', 'uppy-url')
@ -499,11 +513,20 @@ export default class Transloadit<
}
/**
* Allows Golden Retriever plugin to serialize the Assembly status so we can restore it later
* Mirrors the live Assembly status into plugin state and lets Golden
* Retriever serialize it for restore. `assemblyStatus` is written
* unconditionally when `this.assembly = undefined`, `assemblyResponse`
* is undefined and `assemblyStatus` clears too. `lastAssemblyStatus`
* captures the most recent non-null status so the UI can keep displaying
* the previous run's result after `assemblyStatus` clears.
*/
#handleAssemblyStatusUpdate = (
assemblyResponse: AssemblyResponse | undefined,
) => {
if (assemblyResponse != null) {
this.setPluginState({ lastAssemblyStatus: assemblyResponse })
}
this.setPluginState({ assemblyStatus: assemblyResponse })
this.uppy.emit('restore:plugin-data-changed', {
[this.id]: assemblyResponse ? { assemblyResponse } : undefined,
})
@ -650,6 +673,9 @@ export default class Transloadit<
this.uppy.log(err)
}
}
// `assemblyStatus` is cleared automatically when `this.assembly = undefined`
// (via `#cancelAssembly` above, or by `#afterUpload`'s finally block).
// Reset allowNewUpload when upload is cancelled
this.uppy.setState({ allowNewUpload: true })
}
@ -1006,6 +1032,8 @@ export default class Transloadit<
this.uppy.on('restored', this.#onRestored)
this.setPluginState({
assemblyStatus: undefined,
lastAssemblyStatus: undefined,
// Contains file data from Transloadit, indexed by their Transloadit-assigned ID.
files: {},
// Contains result data from Transloadit.

View file

@ -1,6 +1,6 @@
{
"name": "uppy",
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox, Google Drive, S3 and more :dog:",
"version": "5.2.4",
"license": "MIT",
"main": "lib/index.js",
@ -55,7 +55,6 @@
"@uppy/google-photos-picker": "workspace:*",
"@uppy/image-editor": "workspace:*",
"@uppy/image-generator": "workspace:*",
"@uppy/instagram": "workspace:*",
"@uppy/locales": "workspace:*",
"@uppy/onedrive": "workspace:*",
"@uppy/provider-views": "workspace:*",

View file

@ -35,7 +35,6 @@ export { default as GoogleDrive } from '@uppy/google-drive'
export { default as GoogleDrivePicker } from '@uppy/google-drive-picker'
export { default as GooglePhotosPicker } from '@uppy/google-photos-picker'
export { default as ImageEditor } from '@uppy/image-editor'
export { default as Instagram } from '@uppy/instagram'
export { default as OneDrive } from '@uppy/onedrive'
export { default as RemoteSources } from '@uppy/remote-sources'
export { default as ScreenCapture } from '@uppy/screen-capture'

View file

@ -17,7 +17,6 @@ export type { FormOptions } from '@uppy/form'
export type { GoldenRetrieverOptions } from '@uppy/golden-retriever'
export type { GoogleDriveOptions } from '@uppy/google-drive'
export type { ImageEditorOptions } from '@uppy/image-editor'
export type { InstagramOptions } from '@uppy/instagram'
export type { OneDriveOptions } from '@uppy/onedrive'
export type { RemoteSourcesOptions } from '@uppy/remote-sources'
export type { ScreenCaptureOptions } from '@uppy/screen-capture'

View file

@ -55,9 +55,6 @@
{
"path": "../@uppy/image-editor/tsconfig.build.json"
},
{
"path": "../@uppy/instagram/tsconfig.build.json"
},
{
"path": "../@uppy/onedrive/tsconfig.build.json"
},

View file

@ -54,9 +54,6 @@
{
"path": "../@uppy/image-editor/tsconfig.build.json"
},
{
"path": "../@uppy/instagram/tsconfig.build.json"
},
{
"path": "../@uppy/onedrive/tsconfig.build.json"
},

View file

@ -122,7 +122,6 @@ export default () => {
companionAllowedHosts,
...getCompanionKeysParams('GOOGLE_DRIVE'),
})
// .use(Instagram, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
// .use(Dropbox, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
// .use(Box, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
// .use(Facebook, { target: Dashboard, companionUrl: COMPANION_URL, companionAllowedHosts })
@ -150,7 +149,6 @@ export default () => {
'Box',
'Dropbox',
'Facebook',
'Instagram',
'OneDrive',
'Unsplash',
'Zoom',

View file

@ -10373,21 +10373,6 @@ __metadata:
languageName: unknown
linkType: soft
"@uppy/instagram@workspace:*, @uppy/instagram@workspace:^, @uppy/instagram@workspace:packages/@uppy/instagram":
version: 0.0.0-use.local
resolution: "@uppy/instagram@workspace:packages/@uppy/instagram"
dependencies:
"@uppy/companion-client": "workspace:^"
"@uppy/core": "workspace:^"
"@uppy/provider-views": "workspace:^"
"@uppy/utils": "workspace:^"
preact: "npm:^10.26.10"
typescript: "npm:^5.8.3"
peerDependencies:
"@uppy/core": "workspace:^"
languageName: unknown
linkType: soft
"@uppy/locales@workspace:*, @uppy/locales@workspace:packages/@uppy/locales":
version: 0.0.0-use.local
resolution: "@uppy/locales@workspace:packages/@uppy/locales"
@ -10488,7 +10473,6 @@ __metadata:
"@uppy/dropbox": "workspace:^"
"@uppy/facebook": "workspace:^"
"@uppy/google-drive": "workspace:^"
"@uppy/instagram": "workspace:^"
"@uppy/onedrive": "workspace:^"
"@uppy/unsplash": "workspace:^"
"@uppy/url": "workspace:^"
@ -23862,7 +23846,6 @@ __metadata:
"@uppy/google-photos-picker": "workspace:*"
"@uppy/image-editor": "workspace:*"
"@uppy/image-generator": "workspace:*"
"@uppy/instagram": "workspace:*"
"@uppy/locales": "workspace:*"
"@uppy/onedrive": "workspace:*"
"@uppy/provider-views": "workspace:*"