├── Blog ├── Image-flipping-extension │ ├── public │ │ └── images │ │ │ ├── ref.txt │ │ │ ├── ext-auth.png │ │ │ ├── ext-settings.png │ │ │ ├── ext-open-menu.png │ │ │ ├── flipped-image-ui.png │ │ │ ├── personal-work-auth.png │ │ │ ├── ext-all-linked-users.png │ │ │ ├── ext-redirect-settings.png │ │ │ └── flipped-img-shared-link.png │ ├── package.json │ ├── app.js │ ├── views │ │ └── index.hbs │ ├── LICENSE │ ├── controller.js │ └── README.md ├── Convert-activity-log-to-CEF-events │ ├── parserSettings.ini │ ├── README.MD │ ├── cefParser.py │ └── license.txt ├── list_all_team_contents │ ├── config.ini.example │ ├── README.md │ ├── list_all_team_contents.py │ └── LICENSE.txt └── performant_upload │ ├── config.ini.example │ ├── README.md │ ├── performant_upload.py │ └── LICENSE.txt ├── Sample Apps ├── Python │ └── file_browser │ │ ├── requirements.txt │ │ ├── .gitignore │ │ ├── templates │ │ └── index.html │ │ └── README.md └── JavaScript │ └── file_browser │ ├── vite.config.js │ ├── index.html │ ├── .gitignore │ ├── src │ ├── main.jsx │ ├── index.css │ ├── App.jsx │ ├── App.css │ ├── components │ │ ├── LoginPage.css │ │ ├── ShareForm.css │ │ ├── LoginPage.jsx │ │ ├── FileBrowser.css │ │ ├── ShareForm.jsx │ │ └── FileBrowser.jsx │ ├── contexts │ │ └── AuthContext.jsx │ └── utils │ │ └── dropboxClient.js │ ├── eslint.config.js │ ├── package.json │ └── README.md └── README.md /Blog/Image-flipping-extension/public/images/ref.txt: -------------------------------------------------------------------------------- 1 | Reference file so github doesn't remove the folder. 2 | Ignore but don't delete -------------------------------------------------------------------------------- /Blog/Convert-activity-log-to-CEF-events/parserSettings.ini: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | dfbToken= 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Sample Apps/Python/file_browser/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.2 2 | requests==2.31.0 3 | python-dotenv==1.0.1 4 | Werkzeug==3.0.1 5 | Jinja2==3.1.3 6 | dropbox==12.0.2 -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/ext-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/ext-auth.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/ext-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/ext-settings.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/ext-open-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/ext-open-menu.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/flipped-image-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/flipped-image-ui.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/personal-work-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/personal-work-auth.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/ext-all-linked-users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/ext-all-linked-users.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/ext-redirect-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/ext-redirect-settings.png -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/public/images/flipped-img-shared-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/Developer-Samples/master/Blog/Image-flipping-extension/public/images/flipped-img-shared-link.png -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dropbox File Browser 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | -------------------------------------------------------------------------------- /Blog/list_all_team_contents/config.ini.example: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # App key to use to retrieve access tokens 3 | # Required when using a refresh token 4 | app_key = 5 | # App secret to use to retrieve access tokens 6 | # Required when using a refresh token not acquired using PKCE 7 | app_secret = 8 | # Refresh token to use to retrieve access tokens for the team 9 | team_refresh_token = 10 | # Member ID of team admin to operate as 11 | # This should start with 'dbmid:' 12 | team_admin_member_id = -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/main.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * main.jsx - Application Entry Point 3 | * This file serves as the main entry point for the React application. 4 | * It initializes the React root and renders the main App component. 5 | */ 6 | 7 | import { StrictMode } from 'react' 8 | import { createRoot } from 'react-dom/client' 9 | import App from './App' 10 | import './index.css' 11 | 12 | // Create and render the root React component 13 | // StrictMode is enabled for additional development checks and warnings 14 | createRoot(document.getElementById('root')).render( 15 | 16 | 17 | 18 | ) 19 | -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample_extension", 3 | "version": "0.0.1", 4 | "description": "Sample code for a file extension server that flips a selected image upside down", 5 | "main": "app.js", 6 | "private": true, 7 | "scripts": { 8 | "start": "node app.js" 9 | }, 10 | "dependencies": { 11 | "dotenv": "^8.2.0", 12 | "dropbox": "^5.1.0", 13 | "express": "^4.17.1", 14 | "express-session": "^1.17.1", 15 | "hbs": "^4.1.1", 16 | "isomorphic-fetch": "^2.2.1", 17 | "jimp": "^0.14.0", 18 | "morgan": "^1.10.0", 19 | "node-cache": "^5.1.1" 20 | }, 21 | "author": "Ruben Rincon - Dropbox 2020", 22 | "license": "Apache-2.0" 23 | } 24 | -------------------------------------------------------------------------------- /Sample Apps/Python/file_browser/.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual Environment 24 | venv/ 25 | env/ 26 | ENV/ 27 | .env 28 | 29 | # Flask 30 | instance/ 31 | .webassets-cache 32 | 33 | # IDE 34 | .idea/ 35 | .vscode/ 36 | *.swp 37 | *.swo 38 | .DS_Store 39 | 40 | # Logs 41 | *.log 42 | logs/ 43 | 44 | # Local development 45 | .env.local 46 | .env.development.local 47 | .env.test.local 48 | .env.production.local 49 | 50 | # Coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | coverage.xml 57 | *.cover -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | export default defineConfig([ 8 | globalIgnores(['dist']), 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | extends: [ 12 | js.configs.recommended, 13 | reactHooks.configs['recommended-latest'], 14 | reactRefresh.configs.vite, 15 | ], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | ecmaFeatures: { jsx: true }, 22 | sourceType: 'module', 23 | }, 24 | }, 25 | rules: { 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | }, 28 | }, 29 | ]) 30 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-browser", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 14 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 15 | "@fortawesome/react-fontawesome": "^0.2.2", 16 | "crypto-js": "^4.2.0", 17 | "dropbox": "^10.34.0", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "react-router-dom": "^7.6.3" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.30.1", 24 | "@types/react": "^19.1.8", 25 | "@types/react-dom": "^19.1.6", 26 | "@vitejs/plugin-react": "^4.6.0", 27 | "eslint": "^9.30.1", 28 | "eslint-plugin-react-hooks": "^5.2.0", 29 | "eslint-plugin-react-refresh": "^0.4.20", 30 | "globals": "^16.3.0", 31 | "vite": "^7.0.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dropbox Developer Samples 2 | 3 | Hi folks! Welcome! 4 | This repo is managed by Dropbox developer relations and is used to organize coding samples and scripts built with the Dropbox API. These examples may be referenced throughout our [developer blog](https://dropbox.tech/developers), [developer guides](https://www.dropbox.com/developers/documentation), or other developer resources. 5 | 6 | All code in this repo is written as samples for education and demonstration only. 7 | 8 | 9 | ## Current samples in repo 10 | 11 | #### Blog 12 | 13 | - [Converting Dropbox activity log to CEF events](https://github.com/dropbox/Developer-Samples/tree/master/Blog/Convert-activity-log-to-CEF-events) 14 | - [Image Flipping Extension Sample](https://github.com/dropbox/Developer-Samples/tree/master/Blog/Image-flipping-extension) 15 | - [Performant Upload Example](https://github.com/dropbox/Developer-Samples/tree/master/Blog/performant_upload) 16 | - [Team Contents Listing Example](https://github.com/dropbox/Developer-Samples/tree/master/Blog/list_all_team_contents) 17 | -------------------------------------------------------------------------------- /Sample Apps/Python/file_browser/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dropbox File Browser 6 | 27 | 28 | 29 | 30 |

Dropbox File Browser

31 | 32 |
33 |

37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({silent: true}); // read values from .env file 2 | 3 | const 4 | PORT = 3000, // Server port 5 | controller = require('./controller.js'), 6 | express = require('express'), 7 | app = express(), 8 | logger = require('morgan'), // Adding logging into the console 9 | hbs = require('hbs'), // Handlebars as view template 10 | session = require('express-session'); // Session management 11 | 12 | // Session management initialization 13 | // Uses local storage, for testing purposes only 14 | app.use(session({ 15 | secret: process.env.SESSION_SECRET, 16 | resave: false, 17 | saveUninitialized: true 18 | })); 19 | 20 | // Temporary images will be placed in a public folder 21 | // This code does not manage cleanup 22 | app.use(express.static('public')); 23 | app.use(logger('dev')); 24 | 25 | // Handlebars set as the template engine 26 | app.set('view engine', 'hbs'); 27 | 28 | // Server routing or endpoints 29 | app.get('/', controller.home); // home route 30 | app.get('/auth', controller.auth); // OAuth redirect route 31 | app.get('/dropbox_file_action',controller.fileAction); // Dropbox file actions route 32 | app.get('/save',controller.saveToDropbox); // Save flipped image to Dropbox 33 | 34 | // Start server 35 | app.listen(PORT, () => console.log(`Extensions app listening on port ${PORT}!`)); 36 | 37 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color: rgba(255, 255, 255, 0.87); 7 | background-color: #242424; 8 | background-image: radial-gradient(circle at top right, rgba(121, 68, 154, 0.13), transparent), 9 | radial-gradient(circle at bottom left, rgba(41, 196, 255, 0.13), transparent); 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | padding: 0; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | background-color: transparent; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Blog/list_all_team_contents/README.md: -------------------------------------------------------------------------------- 1 | # Team Contents Listing Example 2 | 3 | This script demonstrates how to list the contents of all of the namespaces accessible to a connected Dropbox team using the [the Dropbox API](https://www.dropbox.com/developers/documentation/http/overview) via [the Dropbox Python SDK](https://github.com/dropbox/dropbox-sdk-python). 4 | 5 | ### Getting Started 6 | 7 | This script requires Python 3.6+ and a Dropbox app key, app secret, and a refresh token for a team with the team_data.member and files.metadata.read scopes. 8 | 9 | ### Arguments 10 | 11 | Read config.ini.example for a full list of the required parameters and their descriptions. 12 | 13 | While a simple config file is used here as an example, be sure to always store sensitive values such as app secrets and refresh tokens securely. 14 | 15 | ### Running the code 16 | 17 | Example usage: 18 | 19 | Fill in your parameters in a file named config.ini and then run: 20 | ``` 21 | python3 list_all_team_contents.py 22 | ``` 23 | 24 | ### License 25 | 26 | Unless otherwise noted: 27 | 28 | ``` 29 | Copyright (c) 2024 Dropbox, Inc. 30 | 31 | Licensed under the Apache License, Version 2.0 (the "License"); 32 | you may not use this file except in compliance with the License. 33 | You may obtain a copy of the License at 34 | 35 | http://www.apache.org/licenses/LICENSE-2.0 36 | 37 | Unless required by applicable law or agreed to in writing, software 38 | distributed under the License is distributed on an "AS IS" BASIS, 39 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 40 | See the License for the specific language governing permissions and 41 | limitations under the License. 42 | ``` -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/views/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 47 | 48 |

Flip my image

49 |

Hi {{dropbox_name}}!

50 |

This is how your flipped image looks

51 | 52 |
53 |
54 | Original 55 |
56 |

Original

57 |
58 |
59 | 60 |
61 | Flipped 62 |
63 |

Flipped

64 |
65 |
66 |
67 | 68 | Save to Dropbox 69 | 70 | 71 | -------------------------------------------------------------------------------- /Blog/performant_upload/config.ini.example: -------------------------------------------------------------------------------- 1 | [PATHS] 2 | # Local path of folder of files to upload (non-recursive) 3 | local_path = ./files_to_upload 4 | # Remote path of folder in Dropbox to upload to 5 | remote_path = /uploaded_files 6 | 7 | [AUTHORIZATION] 8 | # To upload to an account, you'll need either an access token or a refresh token with 9 | # corresponding app key/secret. The authorization must include the 'files.content.write' scope. 10 | # The access token, refresh token, and app secret should be stored securely and never made public. 11 | 12 | # Access token to use to perform the API calls 13 | access_token = 14 | # Refresh token to use to retrieve access tokens 15 | refresh_token = 16 | # App key to use to retrieve access tokens. Required when using a refresh token 17 | app_key = 18 | # App secret to use to retrieve access tokens 19 | # Required when using a refresh token not acquired using PKCE 20 | app_secret = 21 | 22 | 23 | [LIMITS] 24 | # These three values can be tuned for better (or worse) overall performance 25 | # based on the scenario and environment: 26 | 27 | # The amount of data, in bytes, to send per upload request 28 | # Must be a multiple of 4 MB 29 | # 25165824 bytes is 24 MB (24 * 1024 * 1024) 30 | chunk_size = 25165824 31 | # How many threads to use per batch of upload sessions 32 | # The maximum number of upload sessions that will be run in parallel at one time 33 | batch_thread_count = 20 34 | # How many threads to use per upload session 35 | # The maximum number of threads that will be run in parallel at one time for a single upload session 36 | concurrent_thread_count = 10 37 | # (batch_thread_count * concurrent_thread_count) is the maximum number of threads used at a time 38 | # For a large number of small files, bias batch_thread_count over concurrent_thread_count 39 | # For a small number of large files, bias concurrent_thread_count over batch_thread_count -------------------------------------------------------------------------------- /Blog/performant_upload/README.md: -------------------------------------------------------------------------------- 1 | # Performant Upload Example 2 | 3 | This script demonstrates how to upload a large amount of data to Dropbox using [the Dropbox API](https://www.dropbox.com/developers/documentation/http/overview) via [the Dropbox Python SDK](https://github.com/dropbox/dropbox-sdk-python) in a performant manner. It aims to optimize the transfer speed by performing operations in parallel and using batch calls whenever possible, as described in [the Performance Guide](https://developers.dropbox.com/dbx-performance-guide). 4 | 5 | ### Getting Started 6 | 7 | This script requires Python 3.6+ and a Dropbox access token (or refresh token) with the ability to write files. 8 | 9 | Note that the script is configurable so that it can be tuned for different scenarios and environments. The provided example thread count settings are set high to create many threads to make use of very high bandwidth network connections, but may exhaust more limited machines/connections. 10 | 11 | ### Arguments 12 | 13 | Read config.ini.example for a full list of the required parameters and their descriptions. 14 | 15 | ### Running the code 16 | 17 | Example usage: 18 | Fill in your parameters in a file named config.ini and then run: 19 | ``` 20 | python3 performant_upload.py 21 | ``` 22 | 23 | ### License 24 | 25 | Unless otherwise noted: 26 | 27 | ``` 28 | Copyright (c) 2022 Dropbox, Inc. 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | ``` -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/App.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * App.jsx - Main Application Component 3 | * This file defines the main application structure, routing configuration, 4 | * and protected route implementation for the Dropbox file browser application. 5 | */ 6 | 7 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; 8 | import { AuthProvider, useAuth } from './contexts/AuthContext'; 9 | import LoginPage from './components/LoginPage'; 10 | import FileBrowser from './components/FileBrowser'; 11 | import './App.css'; 12 | 13 | /** 14 | * ProtectedRoute Component 15 | * A wrapper component that protects routes requiring authentication. 16 | * Redirects to the login page if the user is not authenticated. 17 | * 18 | * @param {Object} props - Component props 19 | * @param {React.ReactNode} props.children - Child components to render when authenticated 20 | */ 21 | function ProtectedRoute({ children }) { 22 | const { isAuthenticated, loading } = useAuth(); 23 | 24 | // Show loading state while authentication status is being determined 25 | if (loading) { 26 | return
Loading...
; 27 | } 28 | 29 | // Redirect to login page if not authenticated 30 | if (!isAuthenticated) { 31 | return ; 32 | } 33 | 34 | return children; 35 | } 36 | 37 | /** 38 | * AppRoutes Component 39 | * Defines the application's routing configuration. 40 | * Maps URLs to their corresponding components and handles protected routes. 41 | */ 42 | function AppRoutes() { 43 | return ( 44 | 45 | {/* Login page and OAuth callback handler - public route */} 46 | } /> 47 | } /> 48 | 49 | {/* Protected file browser route - requires authentication */} 50 | 54 | 55 | 56 | } 57 | /> 58 | 59 | ); 60 | } 61 | 62 | /** 63 | * App Component 64 | * The root component of the application. 65 | * Sets up routing and authentication context providers. 66 | */ 67 | function App() { 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | export default App; 78 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | min-width: 320px; 19 | min-height: 100vh; 20 | } 21 | 22 | h1 { 23 | font-size: 3.2em; 24 | line-height: 1.1; 25 | } 26 | 27 | button { 28 | border-radius: 8px; 29 | border: 1px solid transparent; 30 | padding: 0.6em 1.2em; 31 | font-size: 1em; 32 | font-weight: 500; 33 | font-family: inherit; 34 | background-color: #1a1a1a; 35 | cursor: pointer; 36 | transition: border-color 0.25s; 37 | } 38 | 39 | button:hover { 40 | border-color: #646cff; 41 | } 42 | 43 | button:focus, 44 | button:focus-visible { 45 | outline: 4px auto -webkit-focus-ring-color; 46 | } 47 | 48 | /* Shared Button Styles */ 49 | .btn { 50 | padding: 0.75rem 1.5rem; 51 | color: rgba(255, 255, 255, 0.95); 52 | border: 1px solid rgba(255, 255, 255, 0.1); 53 | border-radius: 12px; 54 | cursor: pointer; 55 | font-size: 0.95rem; 56 | transition: all 0.3s ease; 57 | display: inline-flex; 58 | align-items: center; 59 | gap: 0.5rem; 60 | backdrop-filter: blur(4px); 61 | -webkit-backdrop-filter: blur(4px); 62 | } 63 | 64 | .btn:hover { 65 | background: rgba(0, 81, 212, 0.9); 66 | transform: translateY(-2px); 67 | box-shadow: 0 4px 12px rgba(0, 97, 254, 0.2); 68 | } 69 | 70 | .btn:active { 71 | transform: translateY(0); 72 | } 73 | 74 | .btn:disabled { 75 | background: rgba(204, 204, 204, 0.3); 76 | cursor: not-allowed; 77 | transform: none; 78 | box-shadow: none; 79 | } 80 | 81 | .btn-secondary { 82 | background: rgba(51, 51, 51, 0.5); 83 | } 84 | 85 | .btn-secondary:hover { 86 | background: rgba(58, 58, 58, 0.7); 87 | border-color: rgba(255, 255, 255, 0.2); 88 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 89 | } 90 | 91 | .btn-success { 92 | background: rgba(46, 164, 79, 0.8); 93 | } 94 | 95 | .btn-success:hover { 96 | background: rgba(44, 151, 75, 0.9); 97 | box-shadow: 0 4px 12px rgba(46, 164, 79, 0.2); 98 | } 99 | 100 | .btn-icon { 101 | padding: 0.5rem; 102 | width: 36px; 103 | height: 36px; 104 | justify-content: center; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/LoginPage.css: -------------------------------------------------------------------------------- 1 | .login-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | min-height: 100vh; 7 | padding: 2rem; 8 | background: rgba(42, 42, 42, 0.7); 9 | backdrop-filter: blur(10px); 10 | -webkit-backdrop-filter: blur(10px); 11 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); 12 | border: 1px solid rgba(255, 255, 255, 0.1); 13 | } 14 | 15 | h1 { 16 | color: rgba(255, 255, 255, 0.95); 17 | margin-bottom: 2rem; 18 | text-align: center; 19 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 20 | font-size: 2.5rem; 21 | font-weight: 600; 22 | } 23 | 24 | .auth-options { 25 | margin-bottom: 2rem; 26 | padding: 1.5rem; 27 | background: rgba(51, 51, 51, 0.5); 28 | border-radius: 12px; 29 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 30 | border: 1px solid rgba(255, 255, 255, 0.1); 31 | backdrop-filter: blur(4px); 32 | -webkit-backdrop-filter: blur(4px); 33 | width: 100%; 34 | max-width: 600px; 35 | transition: all 0.3s ease; 36 | } 37 | 38 | .auth-options:hover { 39 | background: rgba(56, 56, 56, 0.6); 40 | transform: translateY(-2px); 41 | } 42 | 43 | .offline-access-option { 44 | display: flex; 45 | align-items: center; 46 | gap: 0.75rem; 47 | font-size: 1rem; 48 | color: rgba(255, 255, 255, 0.9); 49 | cursor: pointer; 50 | justify-content: center; 51 | } 52 | 53 | .offline-access-option input[type="checkbox"] { 54 | width: 18px; 55 | height: 18px; 56 | cursor: pointer; 57 | background: rgba(51, 51, 51, 0.5); 58 | border: 1px solid rgba(255, 255, 255, 0.2); 59 | border-radius: 4px; 60 | accent-color: #0061fe; 61 | } 62 | 63 | .login-buttons { 64 | display: flex; 65 | flex-direction: row; 66 | gap: 1.5rem; 67 | width: 100%; 68 | max-width: 600px; 69 | justify-content: center; 70 | } 71 | 72 | .login-buttons .btn { 73 | flex: 1; 74 | max-width: 280px; 75 | padding: 1rem; 76 | font-size: 1.1rem; 77 | background: rgba(0, 97, 254, 0.9); 78 | color: white; 79 | border: none; 80 | border-radius: 12px; 81 | cursor: pointer; 82 | transition: all 0.3s ease; 83 | backdrop-filter: blur(4px); 84 | -webkit-backdrop-filter: blur(4px); 85 | box-shadow: 0 4px 12px rgba(0, 97, 254, 0.2); 86 | } 87 | 88 | .login-buttons .btn:hover { 89 | background: rgba(0, 97, 254, 1); 90 | transform: translateY(-2px); 91 | box-shadow: 0 6px 16px rgba(0, 97, 254, 0.3); 92 | } 93 | 94 | .login-buttons .btn:active { 95 | transform: translateY(0); 96 | box-shadow: 0 2px 8px rgba(0, 97, 254, 0.2); 97 | } 98 | 99 | /* Responsive Design */ 100 | @media (max-width: 768px) { 101 | .login-container { 102 | padding: 1rem; 103 | } 104 | 105 | h1 { 106 | font-size: 2rem; 107 | } 108 | 109 | .login-buttons { 110 | flex-direction: column; 111 | gap: 1rem; 112 | } 113 | 114 | .login-buttons .btn { 115 | max-width: 100%; 116 | } 117 | } -------------------------------------------------------------------------------- /Blog/Convert-activity-log-to-CEF-events/README.MD: -------------------------------------------------------------------------------- 1 | # Convert Dropbox activity log to CEF events 2 | 3 | By Tahsin Islam 4 | April, 2020 5 | 6 | This script can be used to transform Dropbox activity log events retrieved from the [team log endpoint](https://www.dropbox.com/developers/documentation/http/teams#team_log-get_events) to the [CEF standard](https://community.microfocus.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Implementation-Standard/ta-p/1645557?attachment-id=68077) developed by MicroFocus ArcSight. 7 | 8 | ### Getting Started 9 | 10 | To use the script you will need to have a copy of Python 3.0+ and Dropbox Business access token with either the **"Team Auditing"** or **"Team Member File Access"** permission. 11 | 12 | To get started with running the script you will need to paste your access token in the parserSettings.ini file as indicated. 13 | 14 | With your access token put in you can then proceed to run and execute the script simply by calling ```python cefParser.py``` and associated arguments. You can see the full list of arguments possible in the below section. 15 | 16 | 17 | ### Run Examples 18 | 19 | **Objective**: Generate a CSV of CEF events for all single sign on events 20 | ``` 21 | python cefParser.py --output --category sso 22 | ``` 23 | 24 | In this example passing in the argument --output will generate a CSV file of the events pulled.```--category``` or ```-c``` designates you want to filter on an event category and sso (single sign on) is the category filter. 25 | 26 | **Objective**: Send all category of events to designated syslog server 27 | ``` 28 | python cefParser.py --host 121.121.01.10 --port 1234 29 | ``` 30 | 31 | Here you’re sending all events to the host and port you’ve designated, by using the ```--host``` and ```--port``` arguments. 32 | 33 | **Objective**: Send all CEF events from 2018 up to 2019 to designated syslog server 34 | ``` 35 | python cefParser.py --host 121.121.01.10 --port 1234 --start_time 2018-01-01T00:00:00Z --end_time 2019-01-01T00:00:00Z 36 | ``` 37 | 38 | ### Arguments 39 | ```'-c','--category'```: The category of events you want to pull from audit log. If no category is passed all events are pulled. 40 | ```categories: 'apps', 'comments', 'devices', 'domains', 'file\_operations', 'file\_requests', 'groups', 'logins', 'members', 'paper', 'passwords', 'reports', 'sharing', 'showcase', 'sso', 'team\_folders', 'team\_policies', 'team\_profile', 'tfa', 'trusted\_teams' ``` 41 | ```'-l','--limit'```: The max amount of events to pull from the audit log at one time, default is 1000. 42 | ```'--output'```: Passing this will print a csv file of your query to your current directory named dropbox\_cefevents.csv. 43 | ```'--host'```: The host address for the designated syslog destination to send events. 44 | ```'--port'```: The port address for the designated syslog destination to send events. 45 | ```'-st','--start_time'```: The starting time from when to include events (inclusive), format=```%%Y-%%m-%%dT%%H:%%M:%%SZ``` 46 | ```'-et','--end_time'``` : The end time from when to include events (exclusive), format=```%%Y-%%m-%%dT%%H:%%M:%%SZ``` 47 | ```'-csr', '--cursor'```: Pass event cursor from activity log, to pull new events from last query. 48 | 49 | ### Licensing 50 | The script within this folder is covered by the Apache License as described in LICENSE.txt. 51 | 52 | ##### Please carefully note: 53 | 54 | > "Disclaimer of Warranty. [...] the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License." 55 | -------------------------------------------------------------------------------- /Blog/list_all_team_contents/list_all_team_contents.py: -------------------------------------------------------------------------------- 1 | """Example code for listing all entries in all namespaces accessible to a team.""" 2 | 3 | import configparser 4 | 5 | import dropbox 6 | 7 | 8 | def list_all_contents(): 9 | """Lists the contents of each namespace accessible to a team.""" 10 | 11 | config = configparser.ConfigParser() 12 | config.read("config.ini") 13 | 14 | # We'll first get a client for interacting with the team to list the namespaces 15 | with dropbox.DropboxTeam( 16 | app_key=config.get("DEFAULT", "app_key"), 17 | app_secret=config.get("DEFAULT", "app_secret"), 18 | oauth2_refresh_token=config.get("DEFAULT", "team_refresh_token"), 19 | ) as dbx_team: 20 | # The functionality for listing the namespaces may return duplicates, 21 | # so we'll use a set here to ensure uniqueness 22 | namespace_ids = set() 23 | 24 | def handle_namespaces_result(result): 25 | """Processes each page of namespaces.""" 26 | for namespace in result.namespaces: 27 | namespace_ids.add(namespace.namespace_id) 28 | 29 | namespaces_result = dbx_team.team_namespaces_list() 30 | handle_namespaces_result(namespaces_result) 31 | 32 | # The interface for retrieving the list of namespaces is paginated, 33 | # so we need to make sure we retrieve and process every page of results 34 | while namespaces_result.has_more: 35 | namespaces_result = dbx_team.team_namespaces_list_continue( 36 | cursor=namespaces_result.cursor 37 | ) 38 | handle_namespaces_result(namespaces_result) 39 | 40 | namespace_ids = sorted(list(namespace_ids), key=int) 41 | print(f"Received list of {len(namespace_ids)} namespaces for team.") 42 | 43 | # Now that we've retrieved all the namespaces, 44 | # we'll get an admin client to access those namespaces 45 | with dbx_team.as_admin( 46 | team_member_id=config.get("DEFAULT", "team_admin_member_id") 47 | ) as dbx_admin: 48 | 49 | def handle_listing_result(result): 50 | """Processes each page of file/folder entries. 51 | Refer to the documentation for information on how to use these entries: 52 | https://www.dropbox.com/developers/documentation/http/documentation#files-list_folder""" 53 | for entry in result.entries: 54 | print( 55 | f"\tReceived entry of type {type(entry)} " 56 | f"at path: {entry.path_lower}" 57 | ) 58 | 59 | for namespace_id in namespace_ids: 60 | print(f"Listing namespace with ID: {namespace_id}") 61 | 62 | # For each namespace, we can make a client rooted to that namespace 63 | with dbx_admin.with_path_root( 64 | dropbox.common.PathRoot.namespace_id(namespace_id) 65 | ) as dbx_admin_with_ns: 66 | 67 | listing_result = dbx_admin_with_ns.files_list_folder( 68 | # Because this client is rooted to this namespace, 69 | # we use the empty string `""` as the `path` to 70 | # list the root folder of this namespace 71 | path="", 72 | # Request a recursive listing to get nested entries as well 73 | recursive=True, 74 | # Skip mounted folders because they'll be in the namespace list 75 | include_mounted_folders=False 76 | ) 77 | handle_listing_result(listing_result) 78 | 79 | # Just like with getting the list of namespaces, 80 | # the interface for getting the list of contents of a folder is paginated, 81 | # so we need to make sure we retrieve and process every page of results 82 | while listing_result.has_more: 83 | listing_result = dbx_admin_with_ns.files_list_folder_continue( 84 | cursor=listing_result.cursor 85 | ) 86 | handle_listing_result(listing_result) 87 | 88 | 89 | if __name__ == "__main__": 90 | list_all_contents() 91 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/ShareForm.css: -------------------------------------------------------------------------------- 1 | .share-form-overlay { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | background: rgba(0, 0, 0, 0.7); 8 | backdrop-filter: blur(4px); 9 | -webkit-backdrop-filter: blur(4px); 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | z-index: 1000; 14 | } 15 | 16 | .share-form { 17 | background: rgba(42, 42, 42, 0.95); 18 | border-radius: 16px; 19 | padding: 2rem; 20 | width: 100%; 21 | max-width: 500px; 22 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); 23 | border: 1px solid rgba(255, 255, 255, 0.1); 24 | position: relative; 25 | } 26 | 27 | .close-button { 28 | position: absolute; 29 | top: 1rem; 30 | right: 1rem; 31 | background: transparent; 32 | border: none; 33 | color: rgba(255, 255, 255, 0.7); 34 | font-size: 1.5rem; 35 | cursor: pointer; 36 | padding: 0.5rem; 37 | line-height: 1; 38 | } 39 | 40 | .close-button:hover { 41 | color: #fff; 42 | } 43 | 44 | .share-form h2 { 45 | color: rgba(255, 255, 255, 0.95); 46 | margin: 0 0 1.5rem; 47 | font-size: 1.5rem; 48 | font-weight: 600; 49 | } 50 | 51 | .form-group { 52 | margin-bottom: 1.5rem; 53 | } 54 | 55 | .form-group label { 56 | display: block; 57 | color: rgba(255, 255, 255, 0.8); 58 | margin-bottom: 0.5rem; 59 | font-size: 0.95rem; 60 | } 61 | 62 | .form-group select { 63 | width: 100%; 64 | padding: 0.75rem; 65 | background: rgba(51, 51, 51, 0.7); 66 | border: 1px solid rgba(255, 255, 255, 0.1); 67 | border-radius: 8px; 68 | color: rgba(255, 255, 255, 0.9); 69 | font-size: 0.95rem; 70 | transition: all 0.3s ease; 71 | } 72 | 73 | .form-group select:hover:not(:disabled) { 74 | border-color: rgba(0, 97, 254, 0.5); 75 | background: rgba(56, 56, 56, 0.8); 76 | } 77 | 78 | .form-group select:focus { 79 | outline: none; 80 | border-color: #0061fe; 81 | box-shadow: 0 0 0 2px rgba(0, 97, 254, 0.2); 82 | } 83 | 84 | .checkbox-label { 85 | display: flex !important; 86 | align-items: center; 87 | gap: 0.75rem; 88 | cursor: pointer; 89 | } 90 | 91 | .checkbox-label input[type="checkbox"] { 92 | width: 18px; 93 | height: 18px; 94 | accent-color: #0061fe; 95 | cursor: pointer; 96 | } 97 | 98 | .password-input { 99 | margin-top: 0.75rem; 100 | width: 100%; 101 | box-sizing: border-box; 102 | padding: 0.75rem; 103 | background: rgba(51, 51, 51, 0.7); 104 | border: 1px solid rgba(255, 255, 255, 0.1); 105 | border-radius: 8px; 106 | color: rgba(255, 255, 255, 0.9); 107 | font-size: 0.95rem; 108 | } 109 | 110 | .custom-date-input { 111 | margin-top: 0.75rem; 112 | position: relative; 113 | } 114 | 115 | .custom-date-input svg { 116 | position: absolute; 117 | left: 0.75rem; 118 | top: 50%; 119 | transform: translateY(-50%); 120 | color: rgba(255, 255, 255, 0.5); 121 | } 122 | 123 | .custom-date-input input { 124 | width: 100%; 125 | box-sizing: border-box; 126 | padding: 0.75rem 0.75rem 0.75rem 2.5rem; 127 | background: rgba(51, 51, 51, 0.7); 128 | border: 1px solid rgba(255, 255, 255, 0.1); 129 | border-radius: 8px; 130 | color: rgba(255, 255, 255, 0.9); 131 | font-size: 0.95rem; 132 | } 133 | 134 | .shared-link-container { 135 | margin: 1.5rem 0; 136 | display: flex; 137 | gap: 0.75rem; 138 | } 139 | 140 | .shared-link-input { 141 | flex: 1; 142 | padding: 0.75rem; 143 | background: rgba(51, 51, 51, 0.7); 144 | border: 1px solid rgba(255, 255, 255, 0.1); 145 | border-radius: 8px; 146 | color: rgba(255, 255, 255, 0.9); 147 | font-size: 0.95rem; 148 | } 149 | 150 | .error-message { 151 | background: rgba(255, 59, 48, 0.1); 152 | border: 1px solid rgba(255, 59, 48, 0.2); 153 | color: rgb(255, 89, 89); 154 | padding: 0.75rem; 155 | border-radius: 8px; 156 | margin-bottom: 1.5rem; 157 | font-size: 0.95rem; 158 | } 159 | 160 | .form-actions { 161 | display: flex; 162 | gap: 0.75rem; 163 | margin-top: 2rem; 164 | } 165 | 166 | .form-actions button { 167 | padding: 0.75rem 1.5rem; 168 | border-radius: 8px; 169 | font-size: 0.95rem; 170 | font-weight: 500; 171 | display: flex; 172 | align-items: center; 173 | gap: 0.5rem; 174 | transition: all 0.3s ease; 175 | } 176 | 177 | .form-actions button:disabled { 178 | opacity: 0.6; 179 | cursor: not-allowed; 180 | } 181 | 182 | .btn-primary { 183 | background: #0061fe; 184 | color: white; 185 | border: none; 186 | flex: 1; 187 | } 188 | 189 | .btn-primary:hover:not(:disabled) { 190 | background: #0056e4; 191 | transform: translateY(-1px); 192 | } 193 | 194 | .btn-secondary { 195 | background: rgba(255, 255, 255, 0.1); 196 | color: white; 197 | border: 1px solid rgba(255, 255, 255, 0.1); 198 | } 199 | 200 | .btn-secondary:hover:not(:disabled) { 201 | background: rgba(255, 255, 255, 0.15); 202 | transform: translateY(-1px); 203 | } 204 | 205 | .btn-danger { 206 | background: rgba(219, 14, 3, 0.719); 207 | border: 1px solid rgba(255, 59, 48, 0.2); 208 | } 209 | 210 | .btn-danger:hover:not(:disabled) { 211 | background: rgba(255, 59, 48, 0.2); 212 | transform: translateY(-1px); 213 | } 214 | 215 | .access-level-info { 216 | font-size: 0.85em; 217 | color: #666; 218 | margin-top: 4px; 219 | font-style: italic; 220 | } 221 | 222 | /* Responsive Design */ 223 | @media (max-width: 576px) { 224 | .share-form { 225 | margin: 1rem; 226 | padding: 1.5rem; 227 | } 228 | 229 | .form-actions { 230 | flex-direction: column; 231 | } 232 | 233 | .form-actions button { 234 | width: 100%; 235 | } 236 | 237 | .shared-link-container { 238 | flex-direction: column; 239 | } 240 | } -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * LoginPage.jsx - Authentication Entry Point and Callback Handler 3 | * This component provides the initial login interface for both individual and team-based 4 | * Dropbox authentication, and handles the OAuth callback flow. 5 | */ 6 | 7 | import React, { useState, useEffect, useRef } from 'react'; 8 | import { useNavigate, useSearchParams } from 'react-router-dom'; 9 | import { getAuthUrl, getTokensFromCode } from '../utils/dropboxClient'; 10 | import { DROPBOX_CONFIG, useAuth } from '../contexts/AuthContext'; 11 | import './LoginPage.css'; 12 | 13 | /** 14 | * LoginPage Component 15 | * Provides UI for initiating Dropbox authentication flow and handles OAuth callback. 16 | * Supports both individual user and team authentication. 17 | */ 18 | export default function LoginPage() { 19 | const navigate = useNavigate(); 20 | const { handleLogin } = useAuth(); 21 | const [searchParams] = useSearchParams(); 22 | 23 | // State management 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [error, setError] = useState(null); 26 | const [offlineAccess, setOfflineAccess] = useState(false); 27 | 28 | // Prevents multiple token exchange attempts 29 | const hasExchangedCode = useRef(false); 30 | 31 | /** 32 | * Initiates authentication flow by redirecting to Dropbox OAuth page 33 | * @param {boolean} teamAuth - Whether to request team-level permissions 34 | */ 35 | const handleLoginRedirect = async (teamAuth) => { 36 | try { 37 | setIsLoading(true); 38 | // Store offline access preference in session storage before redirect 39 | sessionStorage.setItem('requestedOfflineAccess', offlineAccess.toString()); 40 | 41 | const authUrl = await getAuthUrl( 42 | DROPBOX_CONFIG.clientId, 43 | DROPBOX_CONFIG.redirectUri, 44 | offlineAccess, 45 | teamAuth 46 | ); 47 | window.location.href = authUrl; 48 | } catch (error) { 49 | console.error(`Failed to get ${teamAuth ? 'team' : 'user'} auth URL:`, error); 50 | setError('Failed to start login process. Please try again.'); 51 | } finally { 52 | setIsLoading(false); 53 | } 54 | }; 55 | 56 | /** 57 | * Processes the OAuth callback parameters and exchanges the code for tokens 58 | * Handles various error cases and successful authentication 59 | */ 60 | useEffect(() => { 61 | const handleCallback = async () => { 62 | const code = searchParams.get('code'); 63 | // If no code is present, this is not a callback - show login page 64 | if (!code) { 65 | return; 66 | } 67 | 68 | // Prevent multiple token exchanges 69 | if (hasExchangedCode.current) { 70 | return; 71 | } 72 | 73 | setIsLoading(true); 74 | 75 | const error = searchParams.get('error'); 76 | const errorDescription = searchParams.get('error_description'); 77 | 78 | // Get offline access preference from session storage 79 | const offlineAccess = sessionStorage.getItem('requestedOfflineAccess') === 'true'; 80 | // Clean up session storage 81 | sessionStorage.removeItem('requestedOfflineAccess'); 82 | 83 | // Handle OAuth errors 84 | if (error || errorDescription) { 85 | const errorMessage = errorDescription || error; 86 | console.error('OAuth error:', { error, errorDescription }); 87 | setError(`Authentication failed: ${errorMessage}`); 88 | setIsLoading(false); 89 | return; 90 | } 91 | 92 | try { 93 | // Mark code exchange as in progress 94 | hasExchangedCode.current = true; 95 | // Exchange code for tokens 96 | const tokens = await getTokensFromCode( 97 | DROPBOX_CONFIG.clientId, 98 | code, 99 | DROPBOX_CONFIG.redirectUri 100 | ); 101 | 102 | // Complete authentication and redirect to file browser 103 | handleLogin(tokens, offlineAccess); 104 | navigate('/browser'); 105 | } catch (error) { 106 | console.error('Token exchange error:', error); 107 | setError(`Failed to complete authentication: ${error.message}`); 108 | } finally { 109 | setIsLoading(false); 110 | } 111 | }; 112 | 113 | handleCallback(); 114 | }, [searchParams, handleLogin, navigate]); 115 | 116 | // If there's an error, show error state 117 | if (error) { 118 | return ( 119 |
120 |

Authentication Error

121 |

{error}

122 | 125 |
126 | ); 127 | } 128 | 129 | // If we're processing a callback, show loading state 130 | if (isLoading && searchParams.get('code')) { 131 | return ( 132 |
133 |

Completing authentication...

134 |

Please wait while we complete the authentication process.

135 |
136 | ); 137 | } 138 | 139 | // Show login interface 140 | return ( 141 |
142 |

Dropbox File Browser

143 | 144 | {/* Offline access toggle */} 145 |
146 | 154 |
155 | 156 | {/* Authentication buttons */} 157 |
158 | 165 | 166 | 173 |
174 |
175 | ); 176 | } -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/README.md: -------------------------------------------------------------------------------- 1 | # Dropbox File Browser 2 | 3 | A client-side web application built with the Dropbox JavaScript SDK and React (using Vite) for browsing, viewing, and managing files in a Dropbox account. It demonstrates how to use the Dropbox API with OAuth 2.0, supporting both individual and team accounts. 4 | 5 | ## Features 6 | 7 | ### Core Features 8 | - **Browse and Navigate**: Traverse through your Dropbox folders and files. 9 | - **Download Files**: Download files directly from your Dropbox. 10 | - **Upload Files**: Upload files to the current Dropbox folder. 11 | - **Create Shared Links**: Create and manage shared links for your files and folders. 12 | - **Team Support**: For Dropbox Business accounts, switch between team members to view their files. (Team admin only) 13 | - **Namespace Toggle**: Switch between personal and team spaces when you're a member of a Dropbox team. 14 | - **Breadcrumb Navigation**: Easily navigate the folder hierarchy. 15 | - **File Details**: View file size and last modified date. 16 | - **Real-time Updates**: Automatically sync file changes using long-polling. 17 | 18 | ### Technical Features 19 | - **Secure Authentication**: Uses OAuth 2.0 with PKCE flow for secure client-side authentication. 20 | - **Offline Access**: Optional long-term access via refresh tokens to stay logged in. 21 | - **Team Space Management**: Properly handles path roots for accessing team spaces. 22 | - **Responsive UI**: A clean interface that works on different screen sizes. 23 | 24 | ## Project Structure 25 | 26 | ``` 27 | file_browser/ 28 | ├── .env # Environment variables (you need to create this) 29 | ├── .gitignore # Git ignore rules 30 | ├── index.html # Main HTML entry point 31 | ├── package.json # Project dependencies and scripts 32 | ├── vite.config.js # Vite configuration 33 | └── src/ 34 | ├── components/ # React components 35 | │ ├── FileBrowser.jsx 36 | │ ├── LoginPage.jsx 37 | │ └── ... 38 | ├── contexts/ # React contexts 39 | │ └── AuthContext.jsx 40 | ├── utils/ # Utility functions 41 | │ └── dropboxClient.js 42 | ├── App.jsx # Main application component 43 | └── main.jsx # Application entry point 44 | ``` 45 | 46 | ## Setup 47 | 48 | ### 1. Dropbox API Configuration 49 | 50 | 1. Go to the [Dropbox App Console](https://www.dropbox.com/developers/apps) and click "Create app". 51 | 2. Choose the following settings: 52 | * **API**: "Scoped access" 53 | * **Access type**: "Full Dropbox" 54 | * **Name**: Choose a unique name for your app. 55 | 56 | 3. Configure your new Dropbox app: 57 | * In the **Settings** tab: 58 | * Note your "App key". This will be your `VITE_DROPBOX_APP_KEY`. 59 | * Add `http://localhost:5173/oauth-callback` to "Redirect URIs". 60 | * In the **Permissions** tab, enable these scopes: 61 | * `account_info.read` (For accessing user account information) 62 | * `files.metadata.read` (For listing files and folders) 63 | * `files.content.read` (For downloading files) 64 | * `files.content.write` (For uploading files) 65 | * `sharing.read` (For reading shared link metadata) 66 | * `sharing.write` (For creating and modifying shared links) 67 | * **For Team features, also add:** 68 | * `members.read` (For listing team members) 69 | * `team_data.member` (For switching between team members) 70 | * `team_info.read` (For accessing team information) 71 | 72 | ### 2. Local Environment Setup 73 | 74 | 1. Clone the repository and navigate to the project directory: 75 | ```sh 76 | git clone 77 | cd JavaScript/file_browser 78 | ``` 79 | 80 | 2. Install dependencies: 81 | ```sh 82 | npm install 83 | ``` 84 | 85 | 3. Set up environment variables by creating a `.env` file in the `file_browser` directory: 86 | ```env 87 | VITE_DROPBOX_APP_KEY="YOUR_APP_KEY_FROM_DROPBOX_CONSOLE" 88 | VITE_DROPBOX_REDIRECT_URI="http://localhost:5173/oauth-callback" 89 | ``` 90 | Replace `YOUR_APP_KEY_FROM_DROPBOX_CONSOLE` with your actual Dropbox App key. 91 | 92 | 4. Run the application: 93 | ```sh 94 | npm run dev 95 | ``` 96 | The application will be available at `http://localhost:5173`. 97 | 98 | ## Usage Notes 99 | 100 | ### For personal use: 101 | - Log in with your Dropbox account 102 | - The app will request the necessary permissions 103 | - You can access your personal and team files and folders 104 | 105 | ### For team features: 106 | - You need admin access to a Dropbox Business team 107 | - Log in with your team admin account 108 | - You can switch between team members 109 | 110 | ### Access Token Options: 111 | - **Short-term access (default)**: Uses short-lived access tokens; re-authentication required periodically. 112 | - **Long-term access**: Uses short-lived access tokens with refresh tokens; re-authentication not required. Check "Request long-term access" on login. 113 | 114 | ## Sharing Features 115 | 116 | ### Create shared links for any file or folder 117 | ### Configure sharing settings: 118 | #### Audience Control: 119 | - Public access (anyone with the link) 120 | - Team-only access (only team members) 121 | - No additional access (link doesn't grant additional permissions beyond what users already have) 122 | 123 | #### Security Options: 124 | - Password protection 125 | - Custom expiration dates 126 | - Download permissions 127 | 128 | ### Manage existing shared links: 129 | - View current settings 130 | - Update settings 131 | - Revoke access 132 | 133 | **Note**: In current implementation shared links are created with viewer-only access. This means recipients can view but not edit the shared content. 134 | 135 | ## Prerequisites 136 | 137 | **Node version: 22.12 or above**: Please make sure you have Node version 22.12 or above in order to avoid the error `TypeError: crypto.hash is not a function.` 138 | 139 | ## Dependencies 140 | 141 | This project relies on the following key packages: 142 | - **React**: A JavaScript library for building user interfaces. 143 | - **React Router**: For client-side routing. 144 | - **Dropbox SDK**: For interacting with the Dropbox API. 145 | - **Vite**: As the frontend build tool and development server. 146 | -------------------------------------------------------------------------------- /Sample Apps/Python/file_browser/README.md: -------------------------------------------------------------------------------- 1 | # Dropbox File Browser 2 | 3 | A web-based file browser application built with Flask and the Dropbox Python SDK. This application demonstrates the usage of Dropbox API for both individual users and team accounts. 4 | 5 | ## Features 6 | 7 | ### Core Features 8 | - Browse and navigate through Dropbox folders 9 | - Download files directly from Dropbox 10 | - Upload a file with support for large file sizes (chunked upload) 11 | - Create and manage shared links for files and folders 12 | - Customize sharing settings including: 13 | - Access level (public/team/no additional access) 14 | - Password protection 15 | - Link expiration 16 | - Download permissions 17 | - File size and modification date display 18 | - Breadcrumb navigation for easy folder traversal 19 | - Toggle between personal and team spaces 20 | - Switch between team members' accounts (team admin only) 21 | 22 | ### Technical Features 23 | - OAuth 2 authentication with Dropbox API 24 | - Support for both short-term and long-term access 25 | - Efficient file metadata handling 26 | - Proper path root management for team spaces 27 | - Error handling and session management 28 | - Responsive web interface 29 | 30 | ## Project Structure 31 | 32 | ``` 33 | file_browser/ 34 | ├── app.py # Main Flask application and Dropbox API integration 35 | ├── requirements.txt # Python dependencies 36 | ├── .env # Environment variables (create this file) 37 | ├── templates/ # HTML templates 38 | │ ├── index.html # Landing page with OAuth login options 39 | │ └── file_browser.html # Main file browser interface 40 | └── .gitignore # Git ignore rules 41 | ``` 42 | 43 | ### Key Components: 44 | - `app.py`: Contains all the route handlers and Dropbox API integration logic 45 | - OAuth 2 authentication flow 46 | - File/folder browsing logic 47 | - Team space management 48 | - Download functionality 49 | - File upload functionality 50 | - Shared link creation and management 51 | - Team data transport API call limit handling 52 | - `templates/`: HTML templates with a clean, responsive design 53 | - `index.html`: Simple landing page with user/team login options 54 | - `file_browser.html`: Main interface with file browsing and team management 55 | - `requirements.txt`: Lists required Python packages: 56 | - Flask for web framework 57 | - Dropbox SDK for API integration 58 | - python-dotenv for environment management 59 | 60 | ## Setup 61 | 62 | ### 1. Dropbox API Configuration 63 | 64 | 1. Go to the [Dropbox App Console](https://www.dropbox.com/developers/apps) 65 | 2. Click "Create app" 66 | 3. Choose the following settings: 67 | - Choose an API: "Scoped access" 68 | - Choose the type of access: "Full Dropbox" 69 | - Name your app: Choose a unique name 70 | 71 | 4. Configure your new Dropbox app: 72 | - In the Settings tab: 73 | - Note your "App key" and "App secret" 74 | - Add `http://localhost:5000/oauth/callback` to "Redirect URIs" 75 | 76 | 77 | - In the Permissions tab, enable these permissions: 78 | ``` 79 | account_info.read (For accessing user account information) 80 | files.metadata.read (For listing files and folders) 81 | files.content.read (For downloading files) 82 | files.content.write (For uploading files) 83 | sharing.write (For creating and modifying shared links) 84 | sharing.read (For reading shared link metadata) 85 | 86 | # Additional scopes for team features: 87 | team_info.read (For accessing team information) 88 | team_data.member (For switching between team members) 89 | members.read (For listing team members) 90 | ``` 91 | 92 | > **Important Note**: The user scopes provide access to both individual and team space content. If you're a member of a Dropbox team, you can access your team's shared files and folders with just the basic scopes (`account_info.read`, `files.metadata.read`, and `files.content.read`). The additional team scopes are only needed for admin tasks like switching between team members' accounts. 93 | 94 | ### 2. Local Environment Setup 95 | 96 | 1. Clone the repository and create a virtual environment: 97 | ```bash 98 | python -m venv venv 99 | source venv/bin/activate # On Unix/macOS 100 | # OR 101 | .\venv\Scripts\activate # On Windows 102 | ``` 103 | 104 | 2. Install dependencies: 105 | ```bash 106 | pip install -r requirements.txt 107 | ``` 108 | 109 | 3. Set up environment variables using one of these methods: 110 | 111 | Option A: Using a .env file (recommended for development) 112 | ```bash 113 | # Create a .env file in the project root: 114 | DROPBOX_APP_KEY=your_app_key_from_dropbox_console 115 | DROPBOX_APP_SECRET=your_app_secret_from_dropbox_console 116 | DROPBOX_REDIRECT_URI=http://localhost:5000/oauth/callback 117 | ``` 118 | 119 | Option B: Setting environment variables directly 120 | ```bash 121 | # Unix/macOS: 122 | export DROPBOX_APP_KEY=your_app_key_from_dropbox_console 123 | export DROPBOX_APP_SECRET=your_app_secret_from_dropbox_console 124 | export DROPBOX_REDIRECT_URI=http://localhost:5000/oauth/callback 125 | 126 | # Windows Command Prompt: 127 | set DROPBOX_APP_KEY=your_app_key_from_dropbox_console 128 | set DROPBOX_APP_SECRET=your_app_secret_from_dropbox_console 129 | set DROPBOX_REDIRECT_URI=http://localhost:5000/oauth/callback 130 | 131 | # Windows PowerShell: 132 | $env:DROPBOX_APP_KEY="your_app_key_from_dropbox_console" 133 | $env:DROPBOX_APP_SECRET="your_app_secret_from_dropbox_console" 134 | $env:DROPBOX_REDIRECT_URI="http://localhost:5000/oauth/callback" 135 | ``` 136 | 137 | 4. Run the application: 138 | ```bash 139 | python app.py 140 | ``` 141 | 142 | The application will be available at `http://localhost:5000` 143 | 144 | ### 3. Usage Notes 145 | 146 | - For personal use: 147 | - Log in with your Dropbox account 148 | - The app will request the necessary permissions 149 | - You can access your personal and team files and folders 150 | 151 | - For team features: 152 | - You need admin access to a Dropbox Business team 153 | - Log in with your team admin account 154 | - You can switch between team members 155 | 156 | - Access Token Options: 157 | - Short-term access (default): Uses short-lived access tokens; re-authenticate required periodically. 158 | - Long-term access: Uses short-lived access tokens with refresh tokens; re-authentication not required. Check "Request long-term access" on login. 159 | 160 | ### 4. Sharing Features 161 | 162 | - Create shared links for any file or folder 163 | - Configure sharing settings: 164 | - Audience Control: 165 | - Public access (anyone with the link) 166 | - Team-only access (only team members) 167 | - No additional access (link doesn't grant additional permissions beyond what users already have) 168 | - Security Options: 169 | - Password protection 170 | - Custom expiration dates 171 | - Download permissions 172 | - Manage existing shared links: 173 | - View current settings 174 | - Update settings 175 | - Revoke access 176 | 177 | > **Note**: In current implementation shared links are created with viewer-only access. This means recipients can view but not edit the shared content. 178 | 179 | ## Dependencies 180 | 181 | - Flask 182 | - Dropbox SDK 183 | - python-dotenv 184 | 185 | -------------------------------------------------------------------------------- /Blog/Convert-activity-log-to-CEF-events/cefParser.py: -------------------------------------------------------------------------------- 1 | import requests, json, argparse, csv, datetime, socket, configparser 2 | 3 | #Get audit log events 4 | def getEvents(eventCategory, cursor): 5 | print("Pulling events...") 6 | headers = {"Authorization": "Bearer "+defaultConfig["dfbToken"], "Content-type": "application/json"} 7 | if cursor is not None: 8 | endpoint = "https://api.dropbox.com/2/team_log/get_events/continue" 9 | data = {"cursor": cursor} 10 | else: 11 | data = {"limit": args.limit} 12 | endpoint = "https://api.dropbox.com/2/team_log/get_events" 13 | if eventCategory: 14 | data['category'] = eventCategory 15 | if args.end_time and args.start_time: 16 | data['time'] = {'start_time': args.start_time, 'end_time': args.end_time} 17 | elif args.end_time: 18 | data['time'] = {'end_time': args.end_time} 19 | elif args.start_time: 20 | data['time'] = {'start_time': args.start_time} 21 | response = requests.post(url=endpoint, headers=headers, data=json.dumps(data)) 22 | 23 | try: 24 | response.raise_for_status() 25 | rbody = response.json() 26 | events = rbody['events'] 27 | if rbody["has_more"]: 28 | events = events + getEvents(eventCategory, cursor=rbody["cursor"])[0] 29 | return (events, rbody["cursor"]) 30 | except requests.exceptions.HTTPError as e: 31 | #Print details of non 200 response. Most likely bad token or event category 32 | print (e) 33 | 34 | #Escape string values for CEF 35 | def cleanData(eventData): 36 | data = str(eventData).strip() 37 | if len(data) > 0: 38 | data.replace('\\', '\\\\') 39 | data.replace('=', '\\=') 40 | if data == '{}': 41 | data = '' 42 | return data 43 | 44 | #Make sure correct date format is provided 45 | def validateDate(date): 46 | try: 47 | datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%SZ') 48 | except ValueError: 49 | raise ValueError("Incorrect date format, should be '%Y-%m-%dT%H:%M:%SZ'") 50 | 51 | #Built CEF formatted event from audit log event dictionary 52 | def buildCEFEvent(eventDict): 53 | eventName = eventDict['event_type']['description'] 54 | eventClassId = eventDict['event_type']['.tag'] 55 | category = eventDict['event_category']['.tag'] 56 | eventTS = datetime.datetime.strptime(eventDict['timestamp'],'%Y-%m-%dT%H:%M:%SZ') 57 | eventDetails = eventDict['details'] 58 | eventActor = eventDict['actor'] 59 | eventAssets = eventDict.get('assets',{}) 60 | eventOrigin = eventDict.get('origin', {}) 61 | ipAddress = eventOrigin.get('geo_location',{}).get('ip_address','') 62 | 63 | if eventDict['actor.tag'] in ['admin','user']: 64 | duser = eventDict['actor_info'].get('email','') 65 | duid =eventDict['actor_info'].get('team_member_id','') 66 | else: 67 | duser = '' 68 | duid = '' 69 | 70 | extensionTemplate = "duser={duser} duid={duid} cat={cat} rt={receiptTime} end={end} cs1={cs1}, cs1Label=Details of event cs2={cs2} cs2Label=Details of event actor cs3={cs3} cs3Label=Details of the event origin src={ipAddress} cs4={cs4} cs4Label=Details of event assets" 71 | extensions = extensionTemplate.format(duser=duser, duid=duid, cat=category, receiptTime=eventTS.strftime('%b %d %Y %H:%M:%S'), end=eventTS.strftime('%b %d %Y %H:%M:%S'), 72 | cs1=cleanData(eventDetails), cs2=cleanData(eventActor), cs3=cleanData(eventOrigin), ipAddress=ipAddress, cs4=cleanData(eventAssets)) 73 | cef_event = cefTemplate.format(cefVersion=cefVersion, deviceVendor=deviceVendor, deviceProduct=deviceProduct, deviceVersion=deviceVersion, 74 | eventClassID=eventClassId, name=eventName, severity=severity[category], extensions=extensions) 75 | return cef_event 76 | 77 | #Write formatted cef events out to a csv 78 | def writeEvents(events): 79 | with open('dropbox_cefevents.csv', mode='w') as cefEvents: 80 | print('Writing events to dropbox_cefevents.csv in current directory') 81 | csvwriter = csv.writer(cefEvents) 82 | csvwriter.writerows(events) 83 | 84 | #Send syslog message via UDP 85 | def sendEvents(events, host, port): 86 | level = 6 #Information 87 | facility = 3 #Daemon 88 | syslog_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 89 | for event in events: 90 | data = "<%d>%s" % (level + facility*8, event[0]) 91 | syslog_socket.sendto(data.encode('utf-8'), (host, port)) 92 | 93 | #Pull out the relevant headers and content from raw events 94 | def formatEvents(events): 95 | cef_events = [] 96 | for event in events: 97 | formatEvent = {} 98 | eventHeaders = event.keys() 99 | for header in eventHeaders: 100 | formatEvent[header] = event[header] 101 | 102 | if header == 'actor': 103 | formatEvent[header+'.tag'] = event[header]['.tag'] 104 | formatEvent[header+"_info"] = event[header].get('user', event[header].get('admin')) 105 | 106 | cef = buildCEFEvent(formatEvent) 107 | cef_events.append([cef]) 108 | return cef_events 109 | 110 | def main(): 111 | if args.end_time: 112 | validateDate(args.end_time) 113 | if args.start_time: 114 | validateDate(args.start_time) 115 | result = getEvents(args.category, args.cursor) 116 | events = formatEvents(result[0]) 117 | if args.output == True: 118 | writeEvents(events) 119 | if (args.host and args.port) is not None: 120 | print("Sending " +str(len(events))+ " activity log events") 121 | sendEvents(events, args.host, args.port) 122 | print("Event cursor: "+ result[1]) 123 | 124 | if __name__ == '__main__': 125 | #Setting up argument parser for command-line options 126 | parser = argparse.ArgumentParser(description='Provide your Dropbox Business API App token and pull events from the Dropbox audit log to send to syslog server.') 127 | parser.add_argument('-c','--category', help='The category of events you want to pull from audit log.', 128 | choices=['apps', 'comments', 'devices', 'domains', 'file_operations', 'file_requests', 'groups', 'logins', 'members', 'paper', 129 | 'passwords', 'reports', 'sharing', 'showcase', 'sso', 'team_folders', 'team_policies', 'team_profile', 'tfa', 'trusted_teams'], default='') 130 | parser.add_argument('-l','--limit', help='The max amount of events to pull from the audit log at one time, default is 1000.', default=1000, type=int) 131 | parser.add_argument('--output', help='Passing this will print a csv file of your query to your current directory named dropbox_cefevents.csv', dest='output', action='store_true') 132 | parser.add_argument('--host', help='The host address for the designated syslog destination to send events.',type=str) 133 | parser.add_argument('--port', help='The port address for the designated syslog destination to send events.',type=int) 134 | parser.add_argument('-st','--start_time', help='The starting time from when to include events (inclusive), format="%%Y-%%m-%%dT%%H:%%M:%%SZ"') 135 | parser.add_argument('-et','--end_time', help='The end time from when to include events (exclusive), format="%%Y-%%m-%%dT%%H:%%M:%%SZ"') 136 | parser.add_argument('-csr', '--cursor', help='Pass event cursor from activity log, to pull new events from last query.', default=None) 137 | parser.set_defaults(output=False) 138 | args = parser.parse_args() 139 | 140 | #Reading from settings 141 | config = configparser.ConfigParser() 142 | config.read("parserSettings.ini") 143 | defaultConfig= config['DEFAULT'] 144 | 145 | 146 | #Severity Mapping 147 | severity = {"comments":0, "paper":1, "showcase":1, "file_requests":2, "file_operations":3, "devices":4, "sharing":4, "team_profile":5, "apps":5, 148 | "groups":5, "domains":6, "team_folders":6, "logins": 6, "members": 6, "passwords":7, "reports":7, "sso":7, "trusted_teams":7, "team_policies":9, "tfa":9} 149 | 150 | #Static CEF Info 151 | cefTemplate = 'CEF:{cefVersion}|{deviceVendor}|{deviceProduct}|{deviceVersion}|{eventClassID}|{name}|{severity}|{extensions}' 152 | deviceVendor = 'Dropbox' 153 | cefVersion = '0' 154 | deviceProduct = 'Dropbox Activity Log' 155 | deviceVersion = '1' 156 | main() 157 | -------------------------------------------------------------------------------- /Blog/performant_upload/performant_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Example code for uploading a local folder of files to Dropbox using the Dropbox API in a 4 | performant manner.""" 5 | 6 | from concurrent.futures import ThreadPoolExecutor 7 | 8 | import configparser 9 | import logging 10 | import os 11 | import time 12 | 13 | import dropbox 14 | 15 | logging.basicConfig() 16 | logging.getLogger().setLevel(logging.DEBUG) 17 | 18 | MB = 1024 * 1024 19 | 20 | 21 | def get_client(config): 22 | """Returns the Dropbox client to use to upload files.""" 23 | 24 | if not ( 25 | config.get('AUTHORIZATION', 'access_token', fallback=None) or 26 | config.get('AUTHORIZATION', 'refresh_token', fallback=None) 27 | ): 28 | raise Exception("Either an access token or refresh token/app key is required.") 29 | 30 | if config.get('AUTHORIZATION', 'refresh_token', fallback=None): 31 | if not config.get('AUTHORIZATION', 'app_key', fallback=None): 32 | raise Exception("App key is required when using a refresh token.") 33 | return dropbox.Dropbox( 34 | oauth2_refresh_token=config.get('AUTHORIZATION', 'refresh_token', fallback=None), 35 | app_key=config.get('AUTHORIZATION', 'app_key', fallback=None), 36 | # the app secret is required for refresh tokens not acquired using PKCE 37 | app_secret=config.get('AUTHORIZATION', 'app_secret', fallback=None) 38 | ) 39 | 40 | return dropbox.Dropbox( 41 | oauth2_access_token=config.get('AUTHORIZATION', 'access_token', fallback=None) 42 | ) 43 | 44 | 45 | def collect_files(folder_path): 46 | """Returns the list of files to upload.""" 47 | 48 | folder_path = os.path.expanduser(folder_path) 49 | 50 | # List all of the files inside the specified folder. 51 | files = sorted( 52 | [os.path.join(folder_path, f) 53 | for f in os.listdir(folder_path) 54 | if os.path.isfile(os.path.join(folder_path, f)) # ignores folders 55 | and f not in [".DS_Store", ".localized", ".gitignore"] # ignores system files, etc. 56 | ] 57 | ) 58 | 59 | logging.info(f"Collected {str(len(files))} files for upload: {files}") 60 | 61 | return files 62 | 63 | 64 | def upload_session_appends(client, session_id, source_file_path, config): 65 | """Performs parallelized upload session appends for one file.""" 66 | 67 | futures = [] 68 | 69 | dest_file_name = os.path.basename(source_file_path) 70 | dest_folder = config.get('PATHS', 'remote_path').lstrip("/") 71 | 72 | logging.info(f"Using upload session with ID '{session_id}' for file '{dest_file_name}'.") 73 | 74 | with open(source_file_path, "rb") as local_file: 75 | 76 | file_size = os.path.getsize(source_file_path) 77 | 78 | def append(dest_file_name, data, cursor, close): 79 | logging.debug(f"Appending to upload session with ID '{cursor.session_id}'" 80 | f" for file '{dest_file_name}'" 81 | f" at offset: {str(cursor.offset)}") 82 | client.files_upload_session_append_v2(f=data, 83 | cursor=cursor, 84 | close=close) 85 | logging.debug(f"Done appending to upload session with ID '{cursor.session_id}'" 86 | f" for file '{dest_file_name}'" 87 | f" at offset: {str(cursor.offset)}") 88 | 89 | if file_size > 0: # For non-empty files, start a number of concurrent append calls. 90 | with ThreadPoolExecutor( 91 | max_workers=config.getint('LIMITS', 'concurrent_thread_count') 92 | ) as session_executor: 93 | while local_file.tell() < file_size: 94 | cursor = dropbox.files.UploadSessionCursor( 95 | session_id=session_id, offset=local_file.tell()) 96 | data = local_file.read(config.getint('LIMITS', 'chunk_size')) 97 | close = local_file.tell() == file_size 98 | futures.append( 99 | session_executor.submit(append, dest_file_name, data, cursor, close)) 100 | else: # For empty files, just call append once to close the upload session. 101 | cursor = dropbox.files.UploadSessionCursor(session_id=session_id, offset=0) 102 | append(dest_file_name=dest_file_name, data=None, cursor=cursor, close=True) 103 | 104 | for future in futures: 105 | try: 106 | future.result() 107 | except Exception as append_exception: 108 | logging.error(f"Upload session with ID '{cursor.session_id}' failed.") 109 | raise append_exception 110 | 111 | return dropbox.files.UploadSessionFinishArg( 112 | cursor=dropbox.files.UploadSessionCursor( 113 | session_id=session_id, offset=local_file.tell()), 114 | commit=dropbox.files.CommitInfo(path=f"/{dest_folder}/{dest_file_name}")) 115 | 116 | 117 | def upload_files(client, files, config): 118 | """Performs upload sessions for a batch of files in parallel.""" 119 | 120 | futures = [] 121 | entries = [] 122 | uploaded_size = 0 123 | 124 | assert len(entries) <= 1000, "Max batch size is 1000." 125 | assert config.getint('LIMITS', 'chunk_size') % (4 * MB) == 0, \ 126 | "Chunk size must be a multiple of 4 MB to use concurrent upload sessions" 127 | 128 | logging.info(f"Starting batch of {str(len(files))} upload sessions.") 129 | start_batch_result = client.files_upload_session_start_batch( 130 | num_sessions=len(files), 131 | session_type=dropbox.files.UploadSessionType.concurrent) 132 | 133 | with ThreadPoolExecutor( 134 | max_workers=config.getint('LIMITS', 'batch_thread_count') 135 | ) as batch_executor: 136 | for index, file in enumerate(files): 137 | futures.append( 138 | batch_executor.submit( 139 | upload_session_appends, 140 | client, start_batch_result.session_ids[index], file, config 141 | ) 142 | ) 143 | 144 | for future in futures: 145 | entries.append(future.result()) 146 | uploaded_size += future.result().cursor.offset 147 | 148 | logging.info(f"Finishing batch of {str(len(entries))} entries.") 149 | finish_launch = client.files_upload_session_finish_batch(entries=entries) 150 | 151 | if finish_launch.is_async_job_id(): 152 | logging.info(f"Polling for status of batch of {str(len(entries))} entries...") 153 | while True: 154 | finish_job = client.files_upload_session_finish_batch_check( 155 | async_job_id=finish_launch.get_async_job_id()) 156 | if finish_job.is_in_progress(): 157 | time.sleep(.5) 158 | else: 159 | complete = finish_job.get_complete() 160 | break 161 | if finish_launch.is_complete(): 162 | complete = finish_launch.get_complete() 163 | elif finish_launch.is_other(): 164 | raise Exception("Unknown finish result type!") 165 | 166 | logging.info(f"Finished batch of {str(len(entries))} entries.") 167 | 168 | for index, entry in enumerate(complete.entries): 169 | if entry.is_success(): 170 | logging.info(f"File successfully uploaded to '{entry.get_success().path_lower}'.") 171 | elif entry.is_failure(): 172 | logging.error(f"Commit for path '{entries[index].commit.path}'" 173 | f" failed due to: {entry.get_failure()}") 174 | 175 | return uploaded_size 176 | 177 | 178 | def run_and_time_uploads(): 179 | """Performs and times the uploads for the folder of files.""" 180 | 181 | config = configparser.ConfigParser() 182 | config.read('config.ini') 183 | 184 | client = get_client(config=config) 185 | files = collect_files(folder_path=config.get('PATHS', 'local_path')) 186 | 187 | start_time = time.time() 188 | uploaded_size = upload_files(client=client, files=files, config=config) 189 | end_time = time.time() 190 | 191 | time_elapsed = end_time - start_time 192 | logging.info(f"Uploaded {uploaded_size} bytes in {time_elapsed:.2f} seconds.") 193 | 194 | megabytes_uploaded = uploaded_size / MB 195 | logging.info(f"Approximate overall speed: {megabytes_uploaded / time_elapsed:.2f} MB/s.") 196 | 197 | 198 | if __name__ == '__main__': 199 | run_and_time_uploads() 200 | -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | Apache License 17 | 18 | Version 2.0, January 2004 19 | 20 | http://www.apache.org/licenses/ 21 | 22 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 23 | 24 | 1. Definitions. 25 | 26 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 29 | 30 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 31 | 32 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 35 | 36 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 41 | 42 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 43 | 44 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 45 | 46 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 47 | 48 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 49 | 50 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 51 | 52 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 53 | You must cause any modified files to carry prominent notices stating that You changed the files; and 54 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 55 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 56 | 57 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 58 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 59 | 60 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 61 | 62 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 63 | 64 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 65 | 66 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 67 | 68 | END OF TERMS AND CONDITIONS 69 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/FileBrowser.css: -------------------------------------------------------------------------------- 1 | .file-browser { 2 | max-width: 1200px; 3 | margin: 20px auto; 4 | padding: 2rem; 5 | background: rgba(42, 42, 42, 0.7); 6 | backdrop-filter: blur(10px); 7 | -webkit-backdrop-filter: blur(10px); 8 | min-height: calc(100vh - 40px); 9 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | border-radius: 16px; 12 | } 13 | 14 | .top-bar { 15 | display: flex; 16 | justify-content: space-between; 17 | align-items: center; 18 | margin-bottom: 2rem; 19 | padding-bottom: 1rem; 20 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 21 | } 22 | 23 | .top-bar h2 { 24 | font-size: 1.8rem; 25 | color: rgba(255, 255, 255, 0.95); 26 | margin: 0; 27 | font-weight: 600; 28 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 29 | } 30 | 31 | .top-bar-actions { 32 | display: flex; 33 | align-items: center; 34 | gap: 1rem; 35 | } 36 | 37 | .namespace-toggle { 38 | display: flex; 39 | align-items: center; 40 | gap: 0.5rem; 41 | color: rgba(255, 255, 255, 0.9); 42 | } 43 | 44 | .switch { 45 | position: relative; 46 | display: inline-block; 47 | width: 50px; 48 | height: 28px; 49 | } 50 | 51 | .switch input { 52 | opacity: 0; 53 | width: 0; 54 | height: 0; 55 | } 56 | 57 | .slider { 58 | position: absolute; 59 | cursor: pointer; 60 | top: 0; 61 | left: 0; 62 | right: 0; 63 | bottom: 0; 64 | background-color: #ccc; 65 | transition: .4s; 66 | border-radius: 28px; 67 | } 68 | 69 | .slider:before { 70 | position: absolute; 71 | content: ""; 72 | height: 20px; 73 | width: 20px; 74 | left: 4px; 75 | bottom: 4px; 76 | background-color: white; 77 | transition: .4s; 78 | border-radius: 50%; 79 | } 80 | 81 | input:checked + .slider { 82 | background-color: #0061fe; 83 | } 84 | 85 | input:focus + .slider { 86 | box-shadow: 0 0 1px #0061fe; 87 | } 88 | 89 | input:checked + .slider:before { 90 | transform: translateX(22px); 91 | } 92 | 93 | .switch input:disabled + .slider { 94 | background-color: #ccc; 95 | cursor: not-allowed; 96 | opacity: 0.6; 97 | } 98 | 99 | .switch input:disabled + .slider:before { 100 | background-color: #f5f5f5; 101 | } 102 | 103 | .namespace-toggle.disabled { 104 | opacity: 0.6; 105 | cursor: not-allowed; 106 | } 107 | 108 | .team-selector { 109 | display: flex; 110 | align-items: center; 111 | gap: 0.5rem; 112 | background: rgba(51, 51, 51, 0.5); 113 | padding: 0.75rem; 114 | border-radius: 12px; 115 | border: 1px solid rgba(255, 255, 255, 0.1); 116 | backdrop-filter: blur(4px); 117 | -webkit-backdrop-filter: blur(4px); 118 | min-width: 280px; 119 | } 120 | 121 | .team-loading, 122 | .team-error { 123 | color: rgba(255, 255, 255, 0.9); 124 | font-size: 0.9rem; 125 | padding: 8px 12px; 126 | background: rgba(64, 64, 64, 0.5); 127 | border-radius: 8px; 128 | flex-grow: 1; 129 | } 130 | 131 | .team-loading { 132 | display: flex; 133 | align-items: center; 134 | gap: 0.5rem; 135 | } 136 | 137 | .team-error { 138 | color: #ff6b6b; 139 | } 140 | 141 | .team-icon { 142 | color: rgba(0, 97, 254, 0.9); 143 | } 144 | 145 | .team-selector select { 146 | padding: 8px 12px; 147 | font-size: 0.9rem; 148 | background: rgba(64, 64, 64, 0.5); 149 | color: rgba(255, 255, 255, 0.9); 150 | border: 1px solid rgba(255, 255, 255, 0.1); 151 | border-radius: 8px; 152 | cursor: pointer; 153 | min-width: 220px; 154 | backdrop-filter: blur(4px); 155 | -webkit-backdrop-filter: blur(4px); 156 | transition: all 0.3s ease; 157 | } 158 | 159 | .team-selector select:hover { 160 | background: rgba(80, 80, 80, 0.7); 161 | border-color: rgba(0, 97, 254, 0.5); 162 | } 163 | 164 | .team-selector select:focus { 165 | outline: none; 166 | border-color: rgba(0, 97, 254, 0.7); 167 | box-shadow: 0 0 0 2px rgba(0, 97, 254, 0.2); 168 | } 169 | 170 | .navigation-bar { 171 | display: flex; 172 | align-items: center; 173 | gap: 0.75rem; 174 | background: rgba(51, 51, 51, 0.5); 175 | backdrop-filter: blur(4px); 176 | -webkit-backdrop-filter: blur(4px); 177 | padding: 0.75rem 1rem; 178 | border-radius: 12px; 179 | margin-bottom: 1.5rem; 180 | border: 1px solid rgba(255, 255, 255, 0.1); 181 | } 182 | 183 | .breadcrumbs { 184 | flex-grow: 1; 185 | font-size: 0.95rem; 186 | color: rgba(255, 255, 255, 0.7); 187 | } 188 | 189 | .breadcrumb-part { 190 | color: #0061fe; 191 | cursor: pointer; 192 | padding: 0.25rem 0.5rem; 193 | border-radius: 6px; 194 | transition: all 0.3s ease; 195 | } 196 | 197 | .breadcrumb-part:hover { 198 | background: rgba(64, 64, 64, 0.5); 199 | text-decoration: none; 200 | } 201 | 202 | .upload-section { 203 | margin-bottom: 1.5rem; 204 | padding: 1.5rem; 205 | background: rgba(51, 51, 51, 0.5); 206 | backdrop-filter: blur(4px); 207 | -webkit-backdrop-filter: blur(4px); 208 | border-radius: 12px; 209 | border: 2px dashed rgba(255, 255, 255, 0.1); 210 | transition: all 0.3s ease; 211 | } 212 | 213 | .upload-section:hover { 214 | border-color: rgba(0, 97, 254, 0.5); 215 | background: rgba(56, 56, 56, 0.6); 216 | } 217 | 218 | .file-input { 219 | display: none; 220 | } 221 | 222 | .file-list { 223 | border: 1px solid rgba(255, 255, 255, 0.1); 224 | border-radius: 12px; 225 | overflow: hidden; 226 | background: rgba(51, 51, 51, 0.5); 227 | backdrop-filter: blur(4px); 228 | -webkit-backdrop-filter: blur(4px); 229 | } 230 | 231 | .file-item, 232 | .file-list-header { 233 | display: grid; 234 | grid-template-columns: minmax(0, 1fr) 120px 200px 120px; 235 | align-items: center; 236 | padding: 1rem; 237 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 238 | transition: all 0.3s ease; 239 | gap: 1rem; 240 | } 241 | 242 | .file-list-header { 243 | color: rgba(255, 255, 255, 0.7); 244 | font-size: 0.85rem; 245 | font-weight: 600; 246 | text-transform: uppercase; 247 | letter-spacing: 0.5px; 248 | border-bottom: 1px solid rgba(255, 255, 255, 0.2); 249 | } 250 | 251 | .file-item:hover { 252 | background: rgba(56, 56, 56, 0.6); 253 | } 254 | 255 | .file-item:last-child { 256 | border-bottom: none; 257 | } 258 | 259 | .file-info { 260 | display: flex; 261 | align-items: center; 262 | gap: 0.75rem; 263 | min-width: 0; 264 | } 265 | 266 | .folder-icon { 267 | color: rgba(0, 97, 254, 0.9); 268 | font-size: 1.1rem; 269 | flex-shrink: 0; 270 | } 271 | 272 | .file-icon { 273 | color: rgba(255, 255, 255, 0.6); 274 | font-size: 1.1rem; 275 | flex-shrink: 0; 276 | } 277 | 278 | .file-name { 279 | overflow: hidden; 280 | text-overflow: ellipsis; 281 | white-space: nowrap; 282 | font-size: 0.95rem; 283 | color: rgba(255, 255, 255, 0.9); 284 | } 285 | 286 | .folder-name { 287 | cursor: pointer; 288 | color: rgba(0, 97, 254, 0.9); 289 | font-weight: 500; 290 | } 291 | 292 | .folder-name:hover { 293 | text-decoration: underline; 294 | } 295 | 296 | .file-size, 297 | .header-size, 298 | .file-modified, 299 | .header-modified { 300 | text-align: center; 301 | font-size: 0.9rem; 302 | color: rgba(255, 255, 255, 0.6); 303 | white-space: nowrap; 304 | } 305 | 306 | .header-size, 307 | .header-modified { 308 | color: rgba(255, 255, 255, 0.7); 309 | font-weight: 600; 310 | } 311 | 312 | .file-actions, 313 | .header-actions { 314 | display: flex; 315 | justify-content: flex-end; 316 | gap: 0.5rem; 317 | } 318 | 319 | .loading, 320 | .empty-folder { 321 | text-align: center; 322 | padding: 3rem; 323 | color: rgba(255, 255, 255, 0.6); 324 | font-size: 0.95rem; 325 | background: rgba(51, 51, 51, 0.5); 326 | backdrop-filter: blur(4px); 327 | -webkit-backdrop-filter: blur(4px); 328 | border-radius: 12px; 329 | margin: 1rem 0; 330 | border: 1px solid rgba(255, 255, 255, 0.1); 331 | } 332 | 333 | .error-container { 334 | text-align: center; 335 | padding: 3rem; 336 | background: rgba(58, 42, 42, 0.5); 337 | backdrop-filter: blur(4px); 338 | -webkit-backdrop-filter: blur(4px); 339 | border-radius: 12px; 340 | margin: 1rem 0; 341 | border: 1px solid rgba(255, 77, 77, 0.2); 342 | } 343 | 344 | .error-message { 345 | color: rgba(255, 107, 107, 0.9); 346 | margin-bottom: 1.5rem; 347 | font-size: 0.95rem; 348 | } 349 | 350 | /* Toast Styles */ 351 | .toast { 352 | position: fixed; 353 | top: 20px; 354 | right: 20px; 355 | padding: 12px 24px; 356 | border-radius: 8px; 357 | color: white; 358 | font-size: 0.95rem; 359 | z-index: 1000; 360 | animation: slideIn 0.3s ease-out forwards; 361 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 362 | max-width: 400px; 363 | word-break: break-word; 364 | } 365 | 366 | .toast.success { 367 | background-color: #10B981; 368 | border: 1px solid rgba(16, 185, 129, 0.2); 369 | } 370 | 371 | .toast.error { 372 | background-color: #EF4444; 373 | border: 1px solid rgba(239, 68, 68, 0.2); 374 | } 375 | 376 | @keyframes slideIn { 377 | from { 378 | transform: translateX(100%); 379 | opacity: 0; 380 | } 381 | to { 382 | transform: translateX(0); 383 | opacity: 1; 384 | } 385 | } 386 | 387 | /* Responsive Design */ 388 | @media (max-width: 768px) { 389 | .file-browser { 390 | margin: 0; 391 | padding: 1rem; 392 | border-radius: 0; 393 | } 394 | 395 | .file-item { 396 | grid-template-columns: 1fr; 397 | gap: 0.75rem; 398 | } 399 | 400 | .file-details { 401 | justify-content: flex-start; 402 | } 403 | 404 | .file-actions { 405 | justify-content: flex-start; 406 | } 407 | 408 | .navigation-bar { 409 | flex-wrap: wrap; 410 | } 411 | 412 | .breadcrumbs { 413 | width: 100%; 414 | margin-top: 0.5rem; 415 | } 416 | 417 | .team-selector { 418 | width: 100%; 419 | } 420 | 421 | .team-selector select { 422 | width: 100%; 423 | } 424 | } -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/controller.js: -------------------------------------------------------------------------------- 1 | // Redirect URL to pass to Dropbox. Has to be whitelisted in Dropbox settings 2 | OAUTH_REDIRECT_URL='http://localhost:3000/auth'; 3 | 4 | // Libraries used during authentication 5 | crypto = require('crypto'), // To create random state values for OAuth 6 | NodeCache = require( "node-cache" ), // To cache OAuth state parameter 7 | 8 | // Libraries to manipulate images and the File System 9 | Jimp = require('jimp'), // Library to manipulate images 10 | fs = require('fs'), // To read the file system 11 | 12 | // Dropbox libraries 13 | Dropbox = require('dropbox').Dropbox, 14 | fetch = require('isomorphic-fetch'); 15 | 16 | // Constants required to recreate image paths 17 | const 18 | backend_path = "public/images/", 19 | client_path = "images/"; 20 | 21 | const mycache = new NodeCache(); 22 | 23 | //-- ROUTED FUNCTIONS -- 24 | 25 | module.exports.home = async (req, res)=>{ 26 | 27 | let dbx = getDropboxInstance(req); 28 | 29 | if(!req.session.token){ 30 | authorize(dbx,req,res); 31 | } else { 32 | // If no file selected instruct user to pick one 33 | if(!req.session.dbx_file){ 34 | res.send("You need to pick a file from Dropbox"); 35 | } 36 | // If user was already editing an image, present it 37 | else if(req.session.dbx_file.cached){ 38 | presentImages(req,res); 39 | } 40 | //If user started the edit flow, but was redirected to OAuth, download images 41 | else if (req.session.dbx_file.id){ 42 | prepareImages(dbx,req,res); 43 | } 44 | } 45 | } 46 | 47 | // Redirect from Dropbox after OAuth 48 | module.exports.auth = async (req, res)=>{ 49 | if(req.query.error_description){ 50 | console.log(req.query.error_description); 51 | res.status(500); 52 | return res.send("

Error... yikes!

Check your console!

"); 53 | } 54 | 55 | // OAuth state value is only valid for 10 minutes 56 | // Session that created the state should be the same as the current session 57 | let state = req.query.state; 58 | let session_id = mycache.get(state); 59 | if(!session_id){ 60 | res.status(440); 61 | return res.send("

Authentication timeout, please try again

"); 62 | }else if (session_id != req.session.id){ 63 | res.status(500); 64 | console.log("Authorization flow was started under a different session"); 65 | return res.send("

Error... yikes!

Check your console!

"); 66 | } 67 | 68 | if(req.query.code){ 69 | 70 | let dbx = getDropboxInstance(req); 71 | 72 | try{ 73 | 74 | let token = await dbx.getAccessTokenFromCode(OAUTH_REDIRECT_URL, req.query.code); 75 | 76 | // Store token and invalidate the state 77 | req.session.token = token; 78 | mycache.del(state); 79 | 80 | // Get the root_namespace for the user 81 | // Ensures that this flow works for Dropbox Business users team spaces 82 | // More info https://www.dropbox.com/developers/reference/namespace-guide 83 | dbx.setAccessToken(token); 84 | let account = await dbx.usersGetCurrentAccount(); 85 | req.session.root_namespace_id = account.root_info.root_namespace_id; 86 | 87 | // Additionally save the user name to display it later 88 | req.session.name = account.name.given_name; 89 | 90 | res.redirect('/'); 91 | 92 | }catch(error){ 93 | console.log(error); 94 | res.status(500); 95 | res.send("

Error... yikes!

Check your console!

"); 96 | } 97 | } 98 | } 99 | 100 | // Called when a file action is triggered by Dropbox 101 | module.exports.fileAction = (req,res)=>{ 102 | 103 | // A file_id is required 104 | if(!req.query.file_id){ 105 | res.status(400); 106 | return res.send("

This action requires a file_id

"); 107 | } 108 | 109 | let dbx = getDropboxInstance(req); 110 | 111 | // Store the file_id in the current session 112 | req.session.dbx_file = { 113 | id : req.query.file_id 114 | } 115 | 116 | // If cookies are cleared or session expires we need to authenticate again 117 | if(!req.session.token){ 118 | authorize(dbx,req,res); 119 | }else{ 120 | prepareImages(dbx,req,res); 121 | } 122 | } 123 | 124 | // Saves the edited file in the current session to Dropbox in the same folder 125 | module.exports.saveToDropbox = async (req,res)=>{ 126 | 127 | let dbx = getDropboxInstance(req); 128 | 129 | let file_id = req.session.dbx_file.id; 130 | let file_name = req.session.dbx_file.name; 131 | let path_lower = req.session.dbx_file.path_lower; 132 | 133 | // Append an edited note to the name of the file before the file extension 134 | let dbx_save_path = path_lower.replace(/\./g,"(edited - FlipImage)."); 135 | 136 | // Server location of the flipped image 137 | let flipped_img_path = backend_path + file_id + "_flipped_" + file_name; 138 | 139 | try{ 140 | 141 | let content = fs.readFileSync(flipped_img_path); 142 | 143 | let upload_params = { 144 | 'contents': content, 145 | 'path' : dbx_save_path, 146 | 'strict_conflict': true, // Force to create a copy 147 | 'autorename': true // Autorename if a copy is created 148 | } 149 | 150 | // UPload file and wait for it to be finished 151 | let upload_response = await dbx.filesUpload(upload_params); 152 | 153 | // Grab a link so we can send the user back to Dropbox 154 | let sharedlink_response = await dbx.sharingCreateSharedLinkWithSettings({'path': upload_response.id}); 155 | 156 | // Cleanup the session upon success and redirect user to Dropbox 157 | req.session.dbx_file = null; 158 | res.redirect(sharedlink_response.url); 159 | 160 | }catch(error){ 161 | // You should handle here possible Dropbox related errors 162 | // See Dropbox documentation for possible errors 163 | console.log(error); 164 | res.status(500); 165 | res.send("

Error... yikes!

Check your console!

"); 166 | } 167 | 168 | } 169 | 170 | // -- INTERNAL FUNCTIONS -- 171 | 172 | // Kick starts the OAuth code exchange 173 | function authorize(dbx,req,res,require_role){ 174 | // Create a random state value 175 | let state = crypto.randomBytes(16).toString('hex'); 176 | // Save state and the session id for 10 mins 177 | mycache.set(state, req.session.id, 6000); 178 | // Get authentication URL and redirect 179 | authUrl = dbx.getAuthenticationUrl(OAUTH_REDIRECT_URL, state, 'code'); 180 | // Attach a require_role parameter if present 181 | if(req.query.require_role){ 182 | authUrl = authUrl + "&require_role=" + req.query.require_role; 183 | } 184 | 185 | res.redirect(authUrl); 186 | } 187 | 188 | // Gets a new instance of Dropbox for this user session 189 | function getDropboxInstance(req){ 190 | 191 | let dbx_config = { 192 | fetch: fetch, 193 | clientId: process.env.DBX_APP_KEY, 194 | clientSecret: process.env.DBX_APP_SECRET 195 | }; 196 | 197 | let dbx = new Dropbox(dbx_config); 198 | 199 | // Set the root namespace for this user 200 | // Ensures that this flow works for Dropbox Business users team folders 201 | // More info https://www.dropbox.com/developers/reference/namespace-guide 202 | if(req.session.root_namespace_id){ 203 | dbx.pathRoot = JSON.stringify({".tag": "root", "root": req.session.root_namespace_id}); 204 | } 205 | 206 | if(req.session.token){ 207 | dbx.setAccessToken(req.session.token); 208 | } 209 | 210 | return dbx; 211 | } 212 | 213 | // Downloads the original image from Dropbox and creates a flipped one 214 | // Both will be placed in a public folder that can be reached by client 215 | async function prepareImages(dbx,req,res){ 216 | 217 | try{ 218 | 219 | // Download file using the file_id as path 220 | let response = await dbx.filesDownload({'path':req.session.dbx_file.id}); 221 | 222 | // Additional information needed when file is saved back 223 | let file_name = response.name; 224 | req.session.dbx_file.name = file_name; 225 | 226 | let path_lower = response.path_lower; 227 | req.session.dbx_file.path_lower = path_lower; 228 | 229 | // First download the file and put it in the local public folder 230 | let original_temp_name = req.session.dbx_file.id + "_" + file_name; 231 | fs.writeFileSync(backend_path + original_temp_name, response.fileBinary); 232 | 233 | // Then create a flipped copy using JIMP 234 | let img_copy = await Jimp.read(backend_path + original_temp_name); 235 | await img_copy.flip(true,true); 236 | let flipped_temp_name = req.session.dbx_file.id + "_flipped_" + file_name; 237 | await(img_copy.writeAsync(backend_path + flipped_temp_name)); 238 | 239 | // Indicate files have been downloaded 240 | req.session.dbx_file.cached = true; 241 | 242 | presentImages(req,res); 243 | 244 | }catch(error){ 245 | 246 | // Dropbox tokens are short lived, so if expired grab a new one 247 | if(error.response && error.response.status == 401){ 248 | authorize(req,res); 249 | }else{ 250 | // You should handle here possible Dropbox download related errors 251 | // See Dropbox documentation for possible errors 252 | // https://dropbox.github.io/dropbox-sdk-js/global.html#FilesLookupError 253 | console.log(error); 254 | res.status(500); 255 | res.send("

Error... yikes!

Check your console!

"); 256 | } 257 | } 258 | } 259 | 260 | // Presents the images to the user 261 | function presentImages(req,res){ 262 | 263 | let original_temp_name = req.session.dbx_file.id + "_" + req.session.dbx_file.name; 264 | let flipped_temp_name = req.session.dbx_file.id + "_flipped_" + req.session.dbx_file.name; 265 | 266 | let args = { 267 | dropbox_name: req.session.name, 268 | image_original_path: client_path + original_temp_name, 269 | image_transformed_path: client_path + flipped_temp_name 270 | } 271 | 272 | res.render('index', args); 273 | } -------------------------------------------------------------------------------- /Blog/performant_upload/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Blog/list_all_team_contents/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Blog/Convert-activity-log-to-CEF-events/license.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/contexts/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthContext.jsx - Authentication Context Provider 3 | * This file implements the authentication context and provider for the Dropbox integration. 4 | * It handles user authentication, token management, and team-based authentication features. 5 | */ 6 | 7 | import React, { createContext, useContext, useState, useEffect } from 'react'; 8 | import { 9 | createDropboxClient, 10 | getTeamMembers, 11 | getCurrentAccount, 12 | getAdminMemberId 13 | } from '../utils/dropboxClient'; 14 | 15 | /** 16 | * Dropbox configuration object containing client ID and redirect URI 17 | * Values are loaded from environment variables 18 | */ 19 | export const DROPBOX_CONFIG = { 20 | clientId: import.meta.env.VITE_DROPBOX_APP_KEY, 21 | redirectUri: import.meta.env.VITE_DROPBOX_REDIRECT_URI, 22 | }; 23 | 24 | /** 25 | * Custom hook that syncs state with localStorage 26 | * @param {string} key - localStorage key 27 | * @param {any} initialValue - Initial value if no value exists in localStorage 28 | * @returns {[any, Function]} - State value and setter function 29 | */ 30 | function useLocalStorage(key, initialValue) { 31 | // Initialize state with value from localStorage or initial value 32 | const [value, setValue] = useState(() => { 33 | try { 34 | const item = localStorage.getItem(key); 35 | return item ? JSON.parse(item) : initialValue; 36 | } catch (error) { 37 | console.error(`Error reading localStorage key "${key}":`, error); 38 | return initialValue; 39 | } 40 | }); 41 | 42 | // Update localStorage when state changes 43 | useEffect(() => { 44 | try { 45 | if (value === null || value === undefined) { 46 | localStorage.removeItem(key); 47 | } else { 48 | localStorage.setItem(key, JSON.stringify(value)); 49 | } 50 | } catch (error) { 51 | console.error(`Error writing to localStorage key "${key}":`, error); 52 | } 53 | }, [key, value]); 54 | 55 | return [value, setValue]; 56 | } 57 | 58 | // Create the authentication context 59 | const AuthContext = createContext(null); 60 | 61 | /** 62 | * AuthProvider Component 63 | * Manages authentication state and provides authentication-related functionality 64 | * to the entire application through React Context. 65 | * 66 | * @param {Object} props - Component props 67 | * @param {React.ReactNode} props.children - Child components to be wrapped with auth context 68 | */ 69 | export function AuthProvider({ children }) { 70 | // Authentication state management 71 | const [accessToken, setAccessToken] = useLocalStorage('dropboxAccessToken', null); 72 | const [refreshToken, setRefreshToken] = useLocalStorage('dropboxRefreshToken', null); 73 | const [dropboxClient, setDropboxClient] = useState(null); 74 | const [loading, setLoading] = useState(true); 75 | const [hasOfflineAccess, setHasOfflineAccess] = useLocalStorage('hasOfflineAccess', false); 76 | 77 | // Team authentication state management 78 | const [isTeamAuth, setIsTeamAuth] = useLocalStorage('isTeamAuth', false); 79 | const [teamMembers, setTeamMembers] = useLocalStorage('teamMembers', []); 80 | const [selectedMember, setSelectedMember] = useLocalStorage('selectedMember', null); 81 | const [rootNamespaceId, setRootNamespaceId] = useLocalStorage('rootNamespaceId', null); 82 | const [isViewingRoot, setIsViewingRoot] = useLocalStorage('isViewingRoot', false); 83 | const [currentPath, setCurrentPath] = useLocalStorage('currentPath', ''); 84 | const [pathKey, setPathKey] = useState(0); 85 | const [isLoadingTeamMembers, setIsLoadingTeamMembers] = useState(false); 86 | // Add currentAccount to state 87 | const [currentAccount, setCurrentAccount] = useState(null); 88 | 89 | /** 90 | * Effect hook for initializing the Dropbox client 91 | * Handles both individual and team-based authentication 92 | */ 93 | useEffect(() => { 94 | if (!accessToken) { 95 | setLoading(false); 96 | return; 97 | } 98 | 99 | // Create client for team member access 100 | if (isTeamAuth && selectedMember) { 101 | const memberId = selectedMember.teamMemberId; 102 | 103 | // Always get the current member's account info to get the correct root namespace ID. 104 | const tempClient = createDropboxClient(accessToken, refreshToken, true, memberId); 105 | getCurrentAccount(tempClient) 106 | .then(account => { 107 | setCurrentAccount(account); 108 | const currentMemberRootNamespaceId = account.root_info.root_namespace_id; 109 | 110 | // Calculate the pathRoot for the API call right here, with the fresh ID. 111 | const correctPathRoot = isViewingRoot && currentMemberRootNamespaceId 112 | ? JSON.stringify({ ".tag": "root", "root": currentMemberRootNamespaceId }) 113 | : null; 114 | 115 | // Update the global rootNamespaceId for the UI and subsequent renders. 116 | if (rootNamespaceId !== currentMemberRootNamespaceId) { 117 | setRootNamespaceId(currentMemberRootNamespaceId); 118 | } 119 | const finalClient = createDropboxClient(accessToken, refreshToken, true, memberId, correctPathRoot); 120 | setDropboxClient(finalClient); 121 | }) 122 | .catch(err => { 123 | console.error("Could not get account info for selected member. Proceeding without path root.", err); 124 | 125 | // If the error is 401, it means the token is invalid for this action. 126 | // The best course of action is to log out to force re-authentication. 127 | if (err?.status === 401) { 128 | handleLogout(); 129 | return; // Stop further execution in this broken state 130 | } 131 | 132 | setRootNamespaceId(''); 133 | const finalClient = createDropboxClient(accessToken, refreshToken, true, memberId, null); 134 | setDropboxClient(finalClient); 135 | }) 136 | .finally(() => { 137 | setLoading(false); 138 | }); 139 | 140 | return; 141 | } 142 | 143 | // Initialize team authentication and fetch members 144 | if (isTeamAuth) { 145 | const basicClient = createDropboxClient(accessToken, refreshToken); 146 | setIsLoadingTeamMembers(true); 147 | 148 | Promise.all([getTeamMembers(basicClient), getAdminMemberId(basicClient)]) 149 | .then(([members, adminMemberId]) => { 150 | setTeamMembers(members); 151 | 152 | if (members.length > 0 && !selectedMember) { 153 | const adminMember = members.find(m => m.teamMemberId === adminMemberId); 154 | setSelectedMember(adminMember || members[0]); 155 | } else { 156 | setLoading(false); 157 | } 158 | }) 159 | .catch(error => { 160 | console.error('Failed to fetch team members:', error); 161 | setLoading(false); 162 | }) 163 | .finally(() => { 164 | setIsLoadingTeamMembers(false); 165 | }); 166 | return; 167 | } 168 | 169 | // Create client for individual user access 170 | if (!isTeamAuth) { 171 | const tempClient = createDropboxClient(accessToken, refreshToken); 172 | getCurrentAccount(tempClient).then(account => { 173 | setCurrentAccount(account); // Store the account info 174 | const currentRootNamespaceId = account.root_info.root_namespace_id; 175 | const homeNamespaceId = account.root_info.home_namespace_id; 176 | 177 | // Set rootNamespaceId regardless of whether it matches home namespace 178 | setRootNamespaceId(currentRootNamespaceId); 179 | 180 | // If it's a single user account (root matches home), ensure we're not in root view 181 | if (currentRootNamespaceId === homeNamespaceId) { 182 | setIsViewingRoot(false); 183 | } 184 | 185 | const correctPathRoot = isViewingRoot && currentRootNamespaceId 186 | ? JSON.stringify({ ".tag": "root", "root": currentRootNamespaceId }) 187 | : null; 188 | 189 | const finalClient = createDropboxClient(accessToken, refreshToken, false, null, correctPathRoot); 190 | setDropboxClient(finalClient); 191 | }).catch(err => { 192 | console.error("Could not get account info for individual user.", err); 193 | 194 | if (err?.status === 401) { 195 | handleLogout(); 196 | return; 197 | } 198 | 199 | setRootNamespaceId(''); 200 | const finalClient = createDropboxClient(accessToken, refreshToken, false, null, null); 201 | setDropboxClient(finalClient); 202 | }).finally(() => { 203 | setLoading(false); 204 | }); 205 | } 206 | }, [accessToken, isTeamAuth, selectedMember, isViewingRoot]); 207 | 208 | 209 | /** 210 | * Handles user login 211 | * Stores authentication data in state and local storage 212 | * 213 | * @param {Object} authData - Authentication data from Dropbox 214 | * @param {boolean} offlineAccess - Whether offline access was requested 215 | */ 216 | const handleLogin = (authData, offlineAccess = false) => { 217 | const { accessToken, refreshToken, teamId: newTeamId } = authData; 218 | const isTeam = !!newTeamId; 219 | 220 | // Clear any existing auth data from storage to prevent stale state 221 | handleLogout(); 222 | 223 | setAccessToken(accessToken); 224 | setRefreshToken(refreshToken); 225 | setIsTeamAuth(isTeam); 226 | setHasOfflineAccess(offlineAccess); 227 | }; 228 | 229 | /** 230 | * Handles user logout 231 | * Clears all authentication data from state and local storage 232 | */ 233 | const handleLogout = () => { 234 | setAccessToken(null); 235 | setRefreshToken(null); 236 | setDropboxClient(null); 237 | setIsTeamAuth(false); 238 | setTeamMembers([]); 239 | setSelectedMember(null); 240 | setRootNamespaceId(null); 241 | setIsViewingRoot(false); 242 | setCurrentPath(''); 243 | setHasOfflineAccess(false); 244 | setCurrentAccount(null); 245 | }; 246 | 247 | /** 248 | * Switches the active team member 249 | * Creates a new Dropbox client for the selected team member 250 | * 251 | * @param {Object} member - Selected team member information 252 | */ 253 | const selectTeamMember = (member) => { 254 | if (!member || !isTeamAuth || !accessToken) return; 255 | setSelectedMember(member); 256 | setCurrentPath(''); // Reset path when switching members 257 | setPathKey(prev => prev + 1); 258 | }; 259 | 260 | /** 261 | * Toggles between the user's home view and the root namespace view. 262 | * This is only available if the user is part of a team structure. 263 | */ 264 | const toggleNamespaceView = () => { 265 | setIsViewingRoot(prev => !prev); 266 | setCurrentPath(''); // Reset path when toggling view 267 | setPathKey(prev => prev + 1); 268 | }; 269 | 270 | /** 271 | * Triggers a reset of the pathKey, forcing a re-render of the FileBrowser component 272 | * to reset its currentPath state. 273 | */ 274 | const triggerPathReset = () => { 275 | setPathKey(prev => prev + 1); 276 | }; 277 | 278 | // Context value containing all authentication-related state and functions 279 | const value = { 280 | accessToken, 281 | refreshToken, 282 | dropboxClient, 283 | loading, 284 | handleLogin, 285 | handleLogout, 286 | isAuthenticated: !!accessToken, 287 | isTeamAuth, 288 | teamMembers, 289 | selectedMember, 290 | selectTeamMember, 291 | rootNamespaceId, 292 | isViewingRoot, 293 | toggleNamespaceView, 294 | pathKey, 295 | triggerPathReset, 296 | hasOfflineAccess, 297 | isLoadingTeamMembers, 298 | currentPath, 299 | setCurrentPath, 300 | currentAccount, 301 | }; 302 | 303 | return {children}; 304 | } 305 | 306 | /** 307 | * Custom hook for accessing authentication context 308 | * @returns {Object} Authentication context value 309 | * @throws {Error} If used outside of AuthProvider 310 | */ 311 | export function useAuth() { 312 | const context = useContext(AuthContext); 313 | if (!context) { 314 | throw new Error('useAuth must be used within an AuthProvider'); 315 | } 316 | return context; 317 | } 318 | -------------------------------------------------------------------------------- /Blog/Image-flipping-extension/README.md: -------------------------------------------------------------------------------- 1 | # Image Flipping Extension Sample 2 | 3 | By Ruben Rincon 4 | July, 2020 5 | 6 | ## Background 7 | 8 | This code sample shows how to integrate [Dropbox Extensions](https://www.dropbox.com/lp/developers/reference/extensions-guide) into an application. It implements a minimalist web server able to handle multiple Dropbox users at the same time which receives actions from images in dropbox.com. Every time an action is triggered, it presents the original image selected and an upside down (flipped) version of it, along with a save option. When the user clicks on *save*, the flipped version is saved to Dropbox in the same path as the original image. Finally, a [shared link](https://www.dropbox.com/lp/developers/reference/dbx-sharing-guide) is created for the newly uploaded image, and the user is redirected to it. 9 | 10 | This simplified example flips images upside down, but an app that has more complex file transformation or analysis would follow a similar pattern. 11 | 12 | The code sample uses the following tech stack: 13 | - The server is implemented using [Node.](https://nodejs.org/en/)[js](https://nodejs.org/en/) and [Express](https://expressjs.com/). The minimum version required for Node.JS is 8.2.1 14 | - [Handlebars](https://handlebarsjs.com/), which is a minimalist template engine that allows to load simple HTML within an .hbs file, and pass JavaScript objects to it. 15 | - The Dropbox [JavaScript SDK](https://github.com/dropbox/dropbox-sdk-js) is used to make API calls to Dropbox 16 | 17 | ## Setting up the Dropbox App 18 | 19 | Before you can successfully run the code from this repository, you first need to have a Dropbox application with a registered Extension. If you don’t have an application, create one in the Dropbox developer [App Console](https://www.dropbox.com/developers/apps). Note that only apps with *Full Dropbox* access can register an Extension. 20 | 21 | If you are using a [*Scoped access*](https://www.dropbox.com/lp/developers/reference/oauth-guide#scopes) app, then you will need to mark at least the following permissions: 22 | 23 | - files.content.read 24 | - files.content.write 25 | - sharing.write 26 | 27 | ## Setting up OAuth 28 | This code sample implements OAuth code authentication flow allowing multiple users to authorize this application. For more information about implementing OAuth please refer to our [OAuth Guide](https://www.dropbox.com/lp/developers/reference/oauth-guide). To configure OAuth in your app you need to follow these two steps in the *Settings* tab in the App Console. 29 | 30 | 1. Click on *Enable additional users* if you haven’t already done so, and your app isn’t already in [production mode](https://www.dropbox.com/developers/reference/developer-guide#production-approval) 31 | 32 | 2. Enter the redirect URI http://localhost:3000/auth as shown in the following screenshot 33 | 34 | ![Redirect URI settings for extension sample](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/ext-redirect-settings.png?raw=true) 35 | 36 | 37 | ## Setting up the Extension 38 | 39 | Now that the app is configured, you need to add the Extension. To do that, scroll down the *Settings* page and look for the *Extensions* section. For more information about setting up an Extension check out our [Extensions Guide](https://www.dropbox.com/lp/developers/reference/extensions-guide). 40 | 41 | Add an Extension with the following configuration: 42 | - **Extension URI -** http://localhost:3000/dropbox_file_action 43 | - **What is the main purpose of this extension? -** Opening/Editing files 44 | - **Supported File Types -** .jpg, .jpeg, .png 45 | - **Max File Size (MB) -** 10 46 | 47 | ![Image flipping Extension settings](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/ext-settings.png?raw=true) 48 | 49 | After you save it, click on *Edit* to modify the extension and **uncheck** the *Only me* option in Visibility settings. This will allow the Extension to be presented to users who link their accounts via OAuth in the Open menu and connected apps page. The final configuration of your Extension should look just like this: 50 | 51 | ![Image flipping Extension settings with Visibility set to "All linked users”](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/ext-all-linked-users.png?raw=true) 52 | 53 | Now that the extension is configured, you should be able to see the extension **in your own Dropbox account** even without going through OAuth. To test it, in dropbox.com browse any image with one of the specified file types and then click on **Open→ Connect more apps → ""**. This will trigger the file action and a new tab will be launched with the Extension URI you registered. After you link the app through OAuth, the extension will be displayed directly in the Open Menu and will be listed in the connected apps page as explained later. 54 | 55 | ## Running the code 56 | 57 | Make sure Node.JS is installed on your machine, if that is not the case, you can go to [nodejs.org](https://nodejs.org/en/) and get the latest version. Anything above 8.2.1 would work. 58 | 59 | This code sample consists of the following files: 60 | - **package.json** tracks metadata about node modules that the project needs 61 | - **app.js** entry point and application configuration 62 | - **controller.js** implements the code sample logic 63 | - **views/index.hbs** is the html page presented to user with the two images 64 | 65 | First, you need to add a **.env** file in the root of the project (where the package.json file is). Paste the following content and replace the values with your information from the *Settings* tab of the App Console 66 | 67 | **.env** 68 | ``` 69 | DBX_APP_KEY = '' 70 | DBX_APP_SECRET = '' 71 | SESSION_SECRET = '' 72 | ``` 73 | Open a terminal and at the project level (where the package.json file is located) install the dependencies using: `npm install` 74 | 75 | And then run the server: `npm start` 76 | 77 | When a user navigates to our deployed server at http://localhost:3000, it will automatically kick off the Dropbox OAuth flow. Once authorized, the user will be redirected to a landing page asking them to go to dropbox.com and select a file. 78 | 79 | At this point, any Dropbox account should be able to authorize this app in your local machine and see the sample extension on the Open dropdown of an image file. If you want to test several Dropbox users at the same time, you can do it in your local machine by running different web browsers or with the same web browser using different browser sessions (such as incognito mode or different Chrome profiles). You can also use localhost tunneling tools such as [ngrok](https://ngrok.com/) to test outside your local machine. 80 | 81 | ![Image flipping Extension authorization page](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/ext-auth.png?raw=true) 82 | 83 | 84 | Now that the app has been authorized, the extension is available in the Open dropdown next to image files and is also listed in the Connected Apps page in the user’s Dropbox settings. When the extension is clicked from an image file, it will redirect them to the server we defined in the Extension settings and start the workflow in the images below. 85 | 86 | ![Sample Extension displayed on a file’s Open menu](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/ext-open-menu.png?raw=true) 87 | 88 | ![User interface for “Flip my image” sample Extension](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/flipped-image-ui.png?raw=true) 89 | 90 | When *Save to Dropbox* is clicked*,* the flipped image will be saved to Dropbox with the suffix (edited - FlipImage). The server also creates a Dropbox shared link and redirects the user to the link. 91 | 92 | ![Flipped image displayed through a Dropbox shared link](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/flipped-img-shared-link.png?raw=true) 93 | 94 | # Important considerations 95 | 96 | ## OAuth 97 | Extensions will only show up directly on a registered file type after the app has been authorized via OAuth. Authorized extensions can be seen and removed on the [*Connected apps*](https://www.dropbox.com/account/connected_apps) page of a user’s Dropbox settings. Extensions only pass the file identifier to the server, but any further action with the API requires a valid access token. The OAuth code flow presented in this code sample is well explained in a blog post called “[OAuth code flow implementation using Node.js and Dropbox JavaScript SDK](https://dropbox.tech/developers/oauth-code-flow-implementation-using-node-js-and-dropbox-javascript-sdk)”. For more details you can visit our [OAuth guide](https://www.dropbox.com/lp/developers/reference/oauth-guide). 98 | 99 | ## Web Sessions 100 | Web sessions are fundamental in this code sample as they allow the server to manage multiple users at the same time independently. Any information relevant to the user’s workflow is stored in a temporary session on the server side, including the Dropbox access token, file and user information. 101 | 102 | A session allows a user to resume an earlier workflow at any point. For instance, if you start an edit flow and close the window or navigate away, you can simply navigate back to http://localhost:3000/ to pick up where you left off. 103 | 104 | In this code example, we’re using the [express-session](https://www.npmjs.com/package/express-session) library. By default, it uses local storage to save session information, which should **never be used on a production environment**. You’ll notice that once the server stops, the local storage is cleaned up. You can find compatible production stores in the library documentation. Additionally, we use non-secure cookies, which also shouldn’t be used in a production environment. 105 | 106 | ## Re-authorizing 107 | As tokens are stored in a web session, they will be lost whenever users clean up cookies or when the current session expires. In addition, Dropbox scoped apps have rolled out [short lived tokens](https://www.dropbox.com/lp/developers/reference/oauth-guide#using-refresh-tokens), so these can be valid only for a few hours. 108 | 109 | For both cases, expired or non existent tokens in the current session, users will need to be redirected again to the authorization process. The good news is that whenever a user has already authorized the app, they will be immediately redirected to the *Redirect URI* and bypass the authorization screen as long as the server has an **https** scheme. Because we use http on the localhost server in this example, you’ll see the authorization screen again each time you need to re-authorize. To test with https, you can use some localhost tunneling tool such as [ngrok](https://ngrok.com/), in which case you need to register the ngrok https redirect URL in the Dropbox App Settings page. 110 | 111 | ## Personal and work accounts 112 | 113 | Some Dropbox users have a personal and work accounts linked together. For this case, at the authorization step the user will need to pick between either of those two. When an action is originated from a file in Dropbox a `require_role` parameter is passed along indicating which of the two accounts started the request. When the app requires a user to re-authorize as described in the section above, that parameter can be passed in the authorization call to preselect the correct account as shown in the image below. 114 | 115 | ![Authorization screen for users with linked personal and work Dropbox accounts](https://github.com/dropbox/Developer-Samples/blob/master/Blog/Image-flipping-extension/public/images/personal-work-auth.png?raw=true) 116 | 117 | ## Making it work for all Dropbox Business users 118 | 119 | Dropbox Business users may have different folder organizational model as Dropbox has been gradually shifting to a new organization paradigm called [Team Spaces](https://help.dropbox.com/teams-admins/team-member/team-space-overview). You can read more about the two organizational models for Dropbox Business users in our Team File Guide. If we want to allow the user to access *all* content (including their Team Space), then we need to set the user’s namespace to match the team’s `root_namespace_id` to ensure that all content can be reached via the API. 120 | 121 | In this code sample, getting the root namespace ID is done right after authorization in the `auth` method. This `root_namespace_id` is saved into the existing user session: 122 | 123 | ``` 124 | // Get the root_namespace for the user 125 | let account = await dbx.usersGetCurrentAccount(); 126 | req.session.root_namespace_id = account.root_info.root_namespace_id; 127 | ``` 128 | When any new request is received on the server and the Dropbox object is instantiated, we set the `root_namespace_id` using the value stored in the session: 129 | ``` 130 | let dbx = new Dropbox(dbx_config); 131 | // Set the root namespace for this user 132 | if (req.session.root_namespace_id){ 133 | dbx.pathRoot = JSON.stringify({".tag": "root", "root": req.session.root_namespace_id}); 134 | } 135 | if (req.session.token){ 136 | dbx.setAccessToken(req.session.token); 137 | } 138 | ``` 139 | For more information, visit the Dropbox [Namespace Guide](https://www.dropbox.com/developers/reference/namespace-guide). 140 | 141 | # Improvements to make 142 | This code sample was created to demonstrate the interaction between the Dropbox API and a web server when using Dropbox Extensions. It also includes an implementation of OAuth and support sDropbox Business users with both organizational models, Team Spaces and Team Folders. 143 | 144 | This code is meant for educational purposes only. When building for a similar use case in production, additional consideration is needed for the following topics: 145 | 146 | - Cleaning up temporary images stored in the public/images folder 147 | - Better error handling 148 | - Input and output optimization 149 | - Proper session storage and secure cookies 150 | - Using https protocol 151 | 152 | # License 153 | 154 | Apache 2.0 155 | -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/ShareForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { 5 | faCalendar, 6 | faSpinner, 7 | faCopy, 8 | faTrash, 9 | faXmark 10 | } from '@fortawesome/free-solid-svg-icons'; 11 | import { createSharedLink, updateSharedLink, revokeSharedLink } from '../utils/dropboxClient'; 12 | import './ShareForm.css'; 13 | 14 | 15 | class ShareFormErrorBoundary extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { hasError: false, error: null }; 19 | } 20 | 21 | static getDerivedStateFromError(error) { 22 | return { hasError: true, error }; 23 | } 24 | 25 | componentDidCatch(error, errorInfo) { 26 | console.error('ShareForm error:', error, errorInfo); 27 | } 28 | 29 | render() { 30 | if (this.state.hasError) { 31 | return ( 32 |
33 |
34 |

Something went wrong

35 |
36 | {this.state.error?.message || 'An error occurred while loading the share form.'} 37 |
38 |
39 | 42 |
43 |
44 |
45 | ); 46 | } 47 | 48 | return this.props.children; 49 | } 50 | } 51 | 52 | const ACCESS_OPTIONS = [ 53 | { value: 'public', label: 'Anyone with the link' }, 54 | { value: 'team', label: 'Team members only' }, 55 | { value: 'no_one', label: 'No additional access' } 56 | ]; 57 | 58 | const EXPIRY_OPTIONS = [ 59 | { value: 'never', label: 'Never' }, 60 | { value: '1', label: '1 day' }, 61 | { value: '7', label: '7 days' }, 62 | { value: '30', label: '30 days' }, 63 | { value: 'custom', label: 'Custom' } 64 | ]; 65 | 66 | function ShareFormContent({ 67 | isOpen, 68 | onClose, 69 | path, 70 | dropboxClient, 71 | existingSettings = null, 72 | showToast 73 | }) { 74 | // Form state 75 | const [audience, setAudience] = useState(existingSettings?.settings?.audience || 'public'); 76 | const [requirePassword, setRequirePassword] = useState(existingSettings?.settings?.require_password || false); 77 | const [password, setPassword] = useState(''); 78 | const [allowDownload, setAllowDownload] = useState(existingSettings?.settings?.allow_download !== false); 79 | const [expires, setExpires] = useState(existingSettings?.settings?.expires || 'never'); 80 | const [expirationTimestamp, setExpirationTimestamp] = useState(existingSettings?.settings?.expiration_timestamp || ''); 81 | const [sharedLink, setSharedLink] = useState(existingSettings?.url || ''); 82 | 83 | // UI state 84 | const [loading, setLoading] = useState(false); 85 | const [error, setError] = useState(null); 86 | const [linkCopied, setLinkCopied] = useState(false); 87 | // Remove useToast hook since we're getting it as a prop 88 | 89 | useEffect(() => { 90 | if (existingSettings) { 91 | setAudience(existingSettings.settings.audience || 'public'); 92 | setRequirePassword(existingSettings.settings.require_password || false); 93 | setAllowDownload(existingSettings.settings.allow_download !== false); 94 | setExpires(existingSettings.settings.expires || 'never'); 95 | setExpirationTimestamp(existingSettings.settings.expiration_timestamp || ''); 96 | setSharedLink(existingSettings.url || ''); 97 | } 98 | }, [existingSettings]); 99 | 100 | const handleCreateLink = async () => { 101 | try { 102 | setLoading(true); 103 | setError(null); 104 | 105 | // Validate password if password protection is enabled 106 | if (requirePassword && !password) { 107 | setError('Password is required when password protection is enabled'); 108 | return; 109 | } 110 | 111 | // Validate expiration date for custom expiry 112 | if (expires === 'custom') { 113 | if (!expirationTimestamp) { 114 | setError('Please select an expiration date'); 115 | return; 116 | } 117 | const selectedDate = new Date(expirationTimestamp); 118 | const today = new Date(); 119 | today.setHours(0, 0, 0, 0); 120 | if (selectedDate < today) { 121 | setError('Expiration date cannot be in the past'); 122 | return; 123 | } 124 | } 125 | 126 | const settings = { 127 | audience: audience, 128 | require_password: requirePassword, 129 | link_password: requirePassword ? password : undefined, 130 | allow_download: allowDownload, 131 | expires: expires === 'never' ? null : expires, 132 | expiration_timestamp: expires === 'custom' ? expirationTimestamp : null 133 | }; 134 | 135 | const result = await createSharedLink(dropboxClient, path, settings); 136 | setSharedLink(result.url); 137 | setLinkCopied(false); 138 | showToast('Shared link created successfully', 'success'); 139 | } catch (error) { 140 | if (error?.error?.error?.['.tag'] === 'path') { 141 | const pathErrorTag = error?.error?.error?.path['.tag']; 142 | if (pathErrorTag === 'not_found') { 143 | setError('The file or folder no longer exists'); 144 | showToast('The file or folder no longer exists', 'error'); 145 | onClose(); 146 | } else if (pathErrorTag === 'malformed_path') { 147 | setError('The path is invalid'); 148 | showToast('The path is invalid', 'error'); 149 | } 150 | } else { 151 | setError('Failed to create shared link. Please try again.'); 152 | showToast('Failed to create shared link', 'error'); 153 | } 154 | } finally { 155 | setLoading(false); 156 | } 157 | }; 158 | 159 | const handleUpdateLink = async () => { 160 | try { 161 | setLoading(true); 162 | setError(null); 163 | 164 | // Validate password if password protection is enabled 165 | if (requirePassword && !password) { 166 | setError('Password is required when password protection is enabled'); 167 | return; 168 | } 169 | 170 | // Validate expiration date for custom expiry 171 | if (expires === 'custom') { 172 | if (!expirationTimestamp) { 173 | setError('Please select an expiration date'); 174 | return; 175 | } 176 | const selectedDate = new Date(expirationTimestamp); 177 | const today = new Date(); 178 | today.setHours(0, 0, 0, 0); 179 | if (selectedDate < today) { 180 | setError('Expiration date cannot be in the past'); 181 | return; 182 | } 183 | } 184 | 185 | const settings = { 186 | audience: audience, 187 | require_password: requirePassword, 188 | link_password: requirePassword ? password : undefined, 189 | allow_download: allowDownload, 190 | expires: expires === 'never' ? null : expires, 191 | expiration_timestamp: expires === 'custom' ? expirationTimestamp : null 192 | }; 193 | 194 | const result = await updateSharedLink(dropboxClient, sharedLink, settings); 195 | setSharedLink(result.url); 196 | setLinkCopied(false); 197 | showToast('Share settings updated successfully', 'success'); 198 | } catch (error) { 199 | if (error?.error?.error?.['.tag'] === 'shared_link_not_found') { 200 | setError('The shared link no longer exists. Create a new one.'); 201 | showToast('The shared link no longer exists. Create a new one.', 'error'); 202 | setSharedLink(''); 203 | } else { 204 | setError('Failed to update share settings. Please try again.'); 205 | showToast('Failed to update share settings', 'error'); 206 | } 207 | } finally { 208 | setLoading(false); 209 | } 210 | }; 211 | 212 | const handleRevokeLink = async () => { 213 | try { 214 | setLoading(true); 215 | setError(null); 216 | 217 | await revokeSharedLink(dropboxClient, sharedLink); 218 | setSharedLink(''); 219 | showToast('Shared link revoked successfully', 'success'); 220 | onClose(); 221 | } catch (error) { 222 | if (error?.error?.error?.['.tag'] === 'shared_link_not_found') { 223 | setSharedLink(''); 224 | showToast('The shared link has already been revoked', 'success'); 225 | onClose(); 226 | } else { 227 | setError('Failed to revoke shared link. Please try again.'); 228 | showToast('Failed to revoke shared link', 'error'); 229 | } 230 | } finally { 231 | setLoading(false); 232 | } 233 | }; 234 | 235 | const handleCopyLink = async () => { 236 | try { 237 | await navigator.clipboard.writeText(sharedLink); 238 | setLinkCopied(true); 239 | showToast('Link copied to clipboard', 'success'); 240 | setTimeout(() => setLinkCopied(false), 2000); 241 | } catch (error) { 242 | console.error('Error copying link:', error); 243 | setError('Failed to copy link to clipboard'); 244 | showToast('Failed to copy link to clipboard', 'error'); 245 | } 246 | }; 247 | 248 | if (!isOpen) return null; 249 | 250 | return ( 251 |
252 |
e.stopPropagation()}> 253 | 256 |

{sharedLink ? 'Update Sharing Settings' : 'Share File'}

257 | 258 | {error && ( 259 |
260 | {error} 261 |
262 | )} 263 | 264 |
265 | 266 | 277 |
278 | Access Level: Viewer (can only view and comment) 279 |
280 |
281 | 282 |
283 | 297 | {requirePassword && ( 298 | setPassword(e.target.value)} 302 | placeholder="Enter password" 303 | disabled={loading} 304 | className="password-input" 305 | /> 306 | )} 307 |
308 | 309 |
310 | 319 |
320 | 321 |
322 | 323 | 334 | {expires === 'custom' && ( 335 |
336 | 337 | setExpirationTimestamp(e.target.value)} 341 | disabled={loading} 342 | min={new Date().toISOString().split('T')[0]} 343 | /> 344 |
345 | )} 346 |
347 | 348 | {sharedLink && ( 349 |
350 | 356 | 364 |
365 | )} 366 | 367 |
368 | {sharedLink ? ( 369 | 372 | ) : ( 373 | 376 | )} 377 | 378 | {sharedLink && ( 379 | 383 | )} 384 |
385 |
386 |
387 | ); 388 | } 389 | 390 | export default function ShareForm(props) { 391 | if (!props.isOpen) return null; 392 | 393 | return createPortal( 394 | 395 | 396 | , 397 | document.body 398 | ); 399 | } -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/components/FileBrowser.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * FileBrowser.jsx - Main File Browser Interface 3 | * This component provides the main interface for browsing and managing Dropbox files. 4 | * It supports file/folder navigation, downloads, uploads, and sharing functionality. 5 | */ 6 | 7 | import { useState, useEffect, useRef, useCallback } from 'react'; 8 | import { useAuth } from '../contexts/AuthContext'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | import { 11 | faFolder, 12 | faFile, 13 | faDownload, 14 | faShare, 15 | faSpinner, 16 | faHome, 17 | faUpload, 18 | faUsers, 19 | faSignOutAlt 20 | } from '@fortawesome/free-solid-svg-icons'; 21 | import { 22 | listFolder, 23 | downloadFile, 24 | uploadFile, 25 | formatBytes, 26 | formatDate, 27 | startLongPoll, 28 | getSharedLinkSettings, 29 | getChanges 30 | } from '../utils/dropboxClient'; 31 | import './FileBrowser.css'; 32 | import ShareForm from './ShareForm'; 33 | 34 | // Toast Component 35 | const Toast = ({ message, type = 'success', onClose }) => { 36 | useEffect(() => { 37 | const timer = setTimeout(() => { 38 | onClose(); 39 | }, 3000); 40 | 41 | return () => clearTimeout(timer); 42 | }, [onClose]); 43 | 44 | return ( 45 |
46 | {message} 47 |
48 | ); 49 | }; 50 | 51 | // Toast Context functionality integrated directly 52 | const useToast = () => { 53 | const [toast, setToast] = useState(null); 54 | 55 | const showToast = useCallback((message, type = 'success') => { 56 | setToast({ message, type }); 57 | }, []); 58 | 59 | const hideToast = useCallback(() => { 60 | setToast(null); 61 | }, []); 62 | 63 | return { toast, showToast, hideToast }; 64 | }; 65 | 66 | /** 67 | * FileBrowser Component 68 | * Main component for browsing and managing Dropbox files and folders. 69 | * Supports both individual and team-based access. 70 | */ 71 | export default function FileBrowser() { 72 | // Authentication and user context 73 | const { 74 | dropboxClient, 75 | handleLogout, 76 | isTeamAuth, 77 | teamMembers, 78 | selectedMember, 79 | selectTeamMember, 80 | isViewingRoot, 81 | toggleNamespaceView, 82 | rootNamespaceId, 83 | isLoadingTeamMembers, 84 | currentPath, 85 | setCurrentPath, 86 | currentAccount 87 | } = useAuth(); 88 | 89 | // Component state 90 | const [entries, setEntries] = useState([]); 91 | const [loading, setLoading] = useState(true); 92 | const [error, setError] = useState(null); 93 | const [uploading, setUploading] = useState(false); 94 | const [cursor, setCursor] = useState(null); 95 | const longPollTimeoutRef = useRef(null); 96 | const isPollingRef = useRef(false); 97 | const backoffTimeoutRef = useRef(null); 98 | const [shareFormOpen, setShareFormOpen] = useState(false); 99 | const [selectedPath, setSelectedPath] = useState(null); 100 | const [existingShareSettings, setExistingShareSettings] = useState(null); 101 | const { toast, showToast, hideToast } = useToast(); 102 | 103 | // Load folder contents when path, client, or selected member changes 104 | useEffect(() => { 105 | if (dropboxClient) { 106 | loadCurrentFolder(); 107 | } 108 | return () => { 109 | // Cleanup timeouts on unmount or path change 110 | if (longPollTimeoutRef.current) { 111 | clearTimeout(longPollTimeoutRef.current); 112 | } 113 | if (backoffTimeoutRef.current) { 114 | clearTimeout(backoffTimeoutRef.current); 115 | } 116 | }; 117 | }, [currentPath, dropboxClient]); 118 | 119 | // Start long polling when cursor changes 120 | useEffect(() => { 121 | if (cursor && dropboxClient && !isPollingRef.current) { 122 | startLongPolling(); 123 | } 124 | return () => { 125 | isPollingRef.current = false; 126 | }; 127 | }, [cursor, dropboxClient]); 128 | 129 | /** 130 | * Starts the long polling process to monitor folder changes 131 | */ 132 | async function startLongPolling() { 133 | if (!cursor || isPollingRef.current) return; 134 | 135 | isPollingRef.current = true; 136 | try { 137 | const result = await startLongPoll(cursor); 138 | 139 | if (result.changes) { 140 | // Instead of reloading the entire folder, fetch only the changes 141 | try { 142 | const changes = await getChanges(dropboxClient, cursor); 143 | 144 | // Update the entries state by processing the changes 145 | setEntries(prevEntries => { 146 | const entriesMap = new Map(prevEntries.map(entry => [entry.path_lower, entry])); 147 | 148 | // Process each change 149 | changes.entries.forEach(change => { 150 | if (change['.tag'] === 'deleted') { 151 | // Remove deleted entries 152 | entriesMap.delete(change.path_lower); 153 | } else { 154 | // Add or update modified entries 155 | entriesMap.set(change.path_lower, change); 156 | } 157 | }); 158 | 159 | // Convert back to array and maintain sorting 160 | return Array.from(entriesMap.values()).sort((a, b) => { 161 | if (a['.tag'] === 'folder' && b['.tag'] !== 'folder') return -1; 162 | if (a['.tag'] !== 'folder' && b['.tag'] === 'folder') return 1; 163 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 164 | }); 165 | }); 166 | 167 | // Update cursor for next polling cycle 168 | setCursor(changes.cursor); 169 | } catch (error) { 170 | // If getting changes fails, fall back to full reload 171 | console.error('Error getting changes, falling back to full reload:', error); 172 | await loadCurrentFolder(); 173 | } 174 | } 175 | 176 | // Handle backoff if specified 177 | if (result.backoff) { 178 | isPollingRef.current = false; 179 | backoffTimeoutRef.current = setTimeout(() => { 180 | startLongPolling(); 181 | }, result.backoff * 1000); 182 | } else { 183 | // No changes, continue polling after a short delay 184 | longPollTimeoutRef.current = setTimeout(() => { 185 | startLongPolling(); 186 | }, 1000); 187 | } 188 | } catch (error) { 189 | console.error('Long polling error:', error); 190 | // On error, retry after a delay 191 | isPollingRef.current = false; 192 | longPollTimeoutRef.current = setTimeout(() => { 193 | startLongPolling(); 194 | }, 5000); // 5 second delay on error 195 | } 196 | } 197 | 198 | /** 199 | * Loads the contents of the current folder 200 | * Updates entries state with files and folders 201 | */ 202 | async function loadCurrentFolder() { 203 | try { 204 | setLoading(true); 205 | setError(null); 206 | 207 | const result = await listFolder(dropboxClient, currentPath); 208 | setEntries(result.entries); 209 | setCursor(result.cursor); 210 | } catch (error) { 211 | // Handle specific API errors 212 | const nestedError = error?.error?.error; 213 | if (nestedError?.['.tag'] === 'path') { 214 | const pathErrorTag = nestedError.path['.tag']; 215 | if (pathErrorTag === 'not_found') { 216 | showToast('The specified file or folder was not found'); 217 | handleNavigateUp(); 218 | } else if (pathErrorTag === 'malformed_path') { 219 | showToast('The specified path is invalid'); 220 | setCurrentPath(''); // Return to root 221 | } 222 | } else { 223 | setError('Failed to load folder contents. Please try again.'); 224 | showToast('Failed to load folder contents', 'error'); 225 | } 226 | } finally { 227 | setLoading(false); 228 | } 229 | } 230 | /** 231 | * Handles file download 232 | * Creates a temporary download link and triggers browser download 233 | * 234 | * @param {string} path - File path in Dropbox 235 | * @param {string} filename - Name of the file to download 236 | */ 237 | async function handleDownload(path, filename) { 238 | try { 239 | const file = await downloadFile(dropboxClient, path); 240 | const downloadUrl = window.URL.createObjectURL(file.fileBlob); 241 | const link = document.createElement('a'); 242 | link.href = downloadUrl; 243 | link.download = filename; 244 | document.body.appendChild(link); 245 | link.click(); 246 | document.body.removeChild(link); 247 | window.URL.revokeObjectURL(downloadUrl); 248 | showToast(`File "${filename}" downloaded successfully`, 'success'); 249 | } catch (error) { 250 | const nestedError = error?.error?.error; 251 | if (nestedError?.['.tag'] === 'path') { 252 | const pathErrorTag = nestedError.path['.tag']; 253 | if (pathErrorTag === 'not_found') { 254 | showToast('The file no longer exists'); 255 | loadCurrentFolder(); // Refresh the folder to update the UI 256 | } else { 257 | showToast('Failed to download file '+ error.message, 'error'); 258 | } 259 | } else { 260 | showToast('Failed to download file '+ error.message, 'error'); 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * Handles file upload 267 | * Uploads selected file to current Dropbox folder 268 | * 269 | * @param {Event} event - File input change event 270 | */ 271 | async function handleUpload(event) { 272 | const file = event.target.files[0]; 273 | if (!file) return; 274 | 275 | try { 276 | setUploading(true); 277 | await uploadFile(dropboxClient, currentPath, file); 278 | showToast(`File "${file.name}" uploaded successfully`, 'success'); 279 | loadCurrentFolder(); 280 | } catch (error) { 281 | showToast('Failed to upload file: ' + error.message, 'error'); 282 | } finally { 283 | setUploading(false); 284 | event.target.value = null; 285 | } 286 | } 287 | 288 | /** 289 | * Handles opening the share form for a file/folder 290 | * @param {string} path - The path of the file/folder to share 291 | */ 292 | async function handleShare(path) { 293 | setSelectedPath(path); 294 | 295 | try { 296 | const existingSettings = await getSharedLinkSettings(dropboxClient, path); 297 | setExistingShareSettings(existingSettings); 298 | } catch (error) { 299 | // If there's an error, log it and reset the settings 300 | console.error('Error fetching shared link settings:', error); 301 | setExistingShareSettings(null); 302 | showToast('Could not fetch existing share settings.', 'error'); 303 | } 304 | setShareFormOpen(true); 305 | } 306 | 307 | /** 308 | * Handles closing the share form 309 | */ 310 | function handleCloseShareForm() { 311 | setShareFormOpen(false); 312 | setSelectedPath(null); 313 | setExistingShareSettings(null); 314 | } 315 | 316 | /** 317 | * Navigates to a folder 318 | * @param {string} path - Target folder path 319 | */ 320 | function handleFolderClick(path) { 321 | setCurrentPath(path); 322 | } 323 | 324 | /** 325 | * Navigates to parent folder 326 | */ 327 | function handleNavigateUp() { 328 | if (!currentPath) return; 329 | const parentPath = currentPath.split('/').slice(0, -1).join('/'); 330 | setCurrentPath(parentPath); 331 | } 332 | 333 | /** 334 | * Generates breadcrumb navigation items 335 | * @returns {string[]} Array of path segments 336 | */ 337 | function getBreadcrumbs() { 338 | if (!currentPath) return []; 339 | return currentPath.split('/').filter(Boolean); 340 | } 341 | 342 | /** 343 | * Handles navigation through breadcrumbs 344 | * @param {number} index - The index in the path array to navigate to 345 | */ 346 | const handleBreadcrumbClick = (index) => { 347 | const pathParts = currentPath.split('/').filter(Boolean); 348 | const newPath = index === -1 ? '' : '/' + pathParts.slice(0, index + 1).join('/'); 349 | setCurrentPath(newPath); 350 | }; 351 | 352 | // Display error state if loading failed 353 | if (error) { 354 | return ( 355 |
356 |

{error}

357 | 360 |
361 | ); 362 | } 363 | 364 | return ( 365 |
366 | {/* Top bar with title and user controls */} 367 |
368 |

Dropbox File Browser

369 |
370 | {/* Namespace view toggle */} 371 |
372 | 381 | Team Space 382 |
383 | 384 | {/* Team member selector for team accounts */} 385 | {isTeamAuth && ( 386 |
387 | 388 | {isLoadingTeamMembers ? ( 389 |
390 | Loading team members... 391 |
392 | ) : teamMembers.length > 0 ? ( 393 | 408 | ) : ( 409 |
No team members found
410 | )} 411 |
412 | )} 413 | 416 |
417 |
418 | 419 | {/* Navigation bar with breadcrumbs */} 420 |
421 | 428 | {currentPath && ( 429 | 436 | )} 437 |
438 | {getBreadcrumbs().map((part, index, array) => ( 439 | 440 | handleBreadcrumbClick(index)} 443 | > 444 | {part} 445 | 446 | {index < array.length - 1 && ' / '} 447 | 448 | ))} 449 |
450 |
451 | 452 | {/* File upload section */} 453 |
454 | 461 | 468 |
469 | 470 | {/* File and folder list */} 471 |
472 | {loading ? ( 473 |
474 | Loading... 475 |
476 | ) : entries.length === 0 ? ( 477 |
This folder is empty
478 | ) : ( 479 | <> 480 |
481 |
Name
482 |
Size
483 |
Modified
484 |
485 |
486 | {entries.map((entry) => ( 487 |
488 |
489 | 493 | entry['.tag'] === 'folder' && handleFolderClick(entry.path_lower)} 496 | > 497 | {entry.name} 498 | 499 |
500 | {entry['.tag'] === 'file' ? ( 501 | <> 502 | {formatBytes(entry.size)} 503 | {formatDate(entry.server_modified)} 504 | 505 | ) : ( 506 | <> 507 | -- 508 | -- 509 | 510 | )} 511 |
512 | {entry['.tag'] === 'file' && ( 513 | <> 514 | 521 | 522 | )} 523 | 530 |
531 |
532 | ))} 533 | 534 | )} 535 |
536 | 537 | {/* Share Form */} 538 | 546 | {/* Toast component */} 547 | {toast && ( 548 | 553 | )} 554 |
555 | ); 556 | } -------------------------------------------------------------------------------- /Sample Apps/JavaScript/file_browser/src/utils/dropboxClient.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dropboxClient.js - Dropbox API Integration Utilities 3 | * This file provides utility functions for interacting with the Dropbox API, 4 | * including authentication, file operations, and team management. 5 | */ 6 | 7 | import { Dropbox, DropboxAuth } from 'dropbox'; 8 | 9 | // ============================================================================ 10 | // Authentication Utilities 11 | // ============================================================================ 12 | 13 | /** 14 | * Generates the OAuth authorization URL 15 | * @param {string} clientId - Dropbox API client ID 16 | * @param {string} redirectUri - OAuth redirect URI 17 | * @param {boolean} offlineAccess - Whether to request offline access (refresh token) 18 | * @param {boolean} teamAuth - Whether to request team-level access 19 | * @returns {Promise} Authorization URL 20 | */ 21 | export async function getAuthUrl(clientId, redirectUri, offlineAccess = false, teamAuth = false) { 22 | const dbxAuth = new DropboxAuth({ clientId }); 23 | const tokenAccessType = offlineAccess ? 'offline' : 'online'; 24 | 25 | const scopes = [ 26 | 'account_info.read', // For accessing user account information 27 | 'files.metadata.read', // For listing files and folders 28 | 'files.content.read', // For downloading files 29 | 'files.content.write', // For uploading files 30 | 'sharing.write', // For creating and modifying shared links 31 | 'sharing.read' // For reading shared link metadata 32 | ]; 33 | 34 | let includeGrantedScopes; 35 | if (teamAuth) { 36 | scopes.push( 37 | 'team_info.read', // For accessing team information 38 | 'members.read', // For listing team members 39 | 'team_data.member' // For switching between team members 40 | ); 41 | includeGrantedScopes = 'team'; 42 | } 43 | 44 | const authUrl = await dbxAuth.getAuthenticationUrl( 45 | redirectUri, 46 | undefined, 47 | 'code', 48 | tokenAccessType, 49 | scopes, 50 | includeGrantedScopes, 51 | true 52 | ); 53 | 54 | const codeVerifier = await dbxAuth.getCodeVerifier(); 55 | sessionStorage.setItem('codeVerifier', codeVerifier); 56 | return authUrl; 57 | } 58 | 59 | /** 60 | * Exchanges an OAuth code for access and refresh tokens 61 | * @param {string} clientId - Dropbox API client ID 62 | * @param {string} code - OAuth authorization code 63 | * @param {string} redirectUri - OAuth redirect URI 64 | * @returns {Promise} Token response object 65 | */ 66 | export async function getTokensFromCode(clientId, code, redirectUri) { 67 | const codeVerifier = sessionStorage.getItem('codeVerifier'); 68 | if (!codeVerifier) { 69 | throw new Error('No code verifier found in session storage'); 70 | } 71 | 72 | sessionStorage.removeItem('codeVerifier'); 73 | 74 | const dbxAuth = new DropboxAuth({ clientId }); 75 | await dbxAuth.setCodeVerifier(codeVerifier); 76 | 77 | try { 78 | const response = await dbxAuth.getAccessTokenFromCode(redirectUri, code); 79 | const { result } = response; 80 | 81 | if (!result.access_token) { 82 | throw new Error('Invalid token response: No access token received'); 83 | } 84 | 85 | // Start with required properties and add optional ones if they exist. 86 | const tokenResponse = { 87 | accessToken: result.access_token, 88 | expiresIn: result.expires_in 89 | }; 90 | 91 | if (result.refresh_token) { 92 | tokenResponse.refreshToken = result.refresh_token; 93 | } 94 | if (result.team_id) { 95 | tokenResponse.teamId = result.team_id; 96 | } 97 | if (result.account_id) { 98 | tokenResponse.accountId = result.account_id; 99 | } 100 | 101 | return tokenResponse; 102 | } catch (error) { 103 | console.error('Token exchange error:', error); 104 | throw error; 105 | } 106 | } 107 | 108 | // ============================================================================ 109 | // Dropbox Client Operations 110 | // ============================================================================ 111 | 112 | /** 113 | * Creates a Dropbox API client instance 114 | * @param {string} accessToken - OAuth access token 115 | * @param {string} [refreshToken] - Optional refresh token for automatic token refresh 116 | * @param {boolean} teamAuth - Whether this is a team auth client 117 | * @param {string} teamMemberId - Team member ID for member-specific operations 118 | * @param {string} pathRoot - Optional path root for team space access 119 | * @returns {Dropbox} Dropbox client instance 120 | */ 121 | export function createDropboxClient( 122 | accessToken, 123 | refreshToken = null, 124 | teamAuth = false, 125 | teamMemberId = null, 126 | pathRoot = null 127 | ) { 128 | const options = { 129 | accessToken: accessToken, 130 | clientId: import.meta.env.VITE_DROPBOX_APP_KEY // Required for refresh token flow 131 | }; 132 | 133 | // Add refresh token if available for automatic refresh 134 | if (refreshToken) { 135 | options.refreshToken = refreshToken; 136 | } 137 | 138 | if (teamAuth && teamMemberId) { 139 | options.selectUser = teamMemberId; 140 | } 141 | 142 | if (pathRoot) { 143 | options.pathRoot = pathRoot; 144 | } 145 | 146 | return new Dropbox(options); 147 | } 148 | 149 | /** 150 | * Fetches current user's account info 151 | * @param {Dropbox} dropboxClient - Dropbox client instance 152 | * @returns {Promise} Current user's account info 153 | */ 154 | export async function getCurrentAccount(dropboxClient) { 155 | try { 156 | const account = await dropboxClient.usersGetCurrentAccount(); 157 | return account.result; 158 | } catch (error) { 159 | console.error('Failed to get current account:', error); 160 | throw error; 161 | } 162 | } 163 | 164 | /** 165 | * Fetches team members list 166 | * @param {Dropbox} dropboxClient - Dropbox client instance 167 | * @returns {Promise} List of team members 168 | */ 169 | export async function getTeamMembers(dropboxClient) { 170 | try { 171 | let allMembers = []; 172 | let response = await dropboxClient.teamMembersListV2({ limit: 100 }); 173 | allMembers = allMembers.concat(response.result.members); 174 | 175 | while (response.result.has_more) { 176 | response = await dropboxClient.teamMembersListContinueV2({ 177 | cursor: response.result.cursor, 178 | }); 179 | allMembers = allMembers.concat(response.result.members); 180 | } 181 | 182 | return allMembers.map(member => ({ 183 | accountId: member.profile.account_id, 184 | email: member.profile.email, 185 | name: member.profile.name.display_name, 186 | teamMemberId: member.profile.team_member_id 187 | })); 188 | } catch (error) { 189 | console.error('Failed to get team members:', error); 190 | throw error; 191 | } 192 | } 193 | 194 | /** 195 | * Get the authenticated admin's member ID. 196 | * @param {DropboxTeam} dbxTeam - DropboxTeam instance. 197 | * @returns {Promise} The admin's team member ID. 198 | */ 199 | export async function getAdminMemberId(dbxTeam) { 200 | try { 201 | const adminProfile = await dbxTeam.teamTokenGetAuthenticatedAdmin(); 202 | return adminProfile.result.admin_profile.team_member_id; 203 | } catch (error) { 204 | console.error('Failed to get admin member ID:', error); 205 | throw error; 206 | } 207 | } 208 | 209 | /** 210 | * Lists contents of a Dropbox folder with proper pagination and caching 211 | * @param {Dropbox} dropboxClient - Dropbox client instance 212 | * @param {string} path - Folder path to list 213 | * @returns {Promise} Folder contents with entries and cursor 214 | */ 215 | export async function listFolder(dropboxClient, path = '') { 216 | try { 217 | let allEntries = []; 218 | let response = await dropboxClient.filesListFolder({ 219 | path, 220 | include_deleted: false 221 | }); 222 | 223 | allEntries = allEntries.concat(response.result.entries); 224 | 225 | // Handle pagination 226 | while (response.result.has_more) { 227 | response = await dropboxClient.filesListFolderContinue({ 228 | cursor: response.result.cursor 229 | }); 230 | allEntries = allEntries.concat(response.result.entries); 231 | } 232 | 233 | return { 234 | entries: allEntries.sort((a, b) => { 235 | // Keep existing sorting logic 236 | if (a['.tag'] === 'folder' && b['.tag'] !== 'folder') return -1; 237 | if (a['.tag'] !== 'folder' && b['.tag'] === 'folder') return 1; 238 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 239 | }), 240 | cursor: response.result.cursor // Keep cursor for long polling 241 | }; 242 | } catch (error) { 243 | throw error; 244 | } 245 | } 246 | 247 | /** 248 | * Downloads a file from Dropbox 249 | * @param {Dropbox} dropboxClient - Dropbox client instance 250 | * @param {string} path - Path to the file 251 | * @returns {Promise} File download result 252 | */ 253 | export async function downloadFile(dropboxClient, path) { 254 | try { 255 | const response = await dropboxClient.filesDownload({ path }); 256 | return response.result; 257 | } catch (error) { 258 | console.error('Error downloading file:', error); 259 | throw error; 260 | } 261 | } 262 | 263 | /** 264 | * Uploads a file to Dropbox 265 | * @param {Dropbox} dropboxClient - Dropbox client instance 266 | * @param {string} path - Destination path 267 | * @param {File} file - File object to upload 268 | * @returns {Promise} Upload result 269 | */ 270 | export async function uploadFile(dropboxClient, path, file) { 271 | 272 | try { 273 | // It is recommended to keep chunk size as multiple of 4MB 274 | const CHUNK_SIZE = 64 * 1024 * 1024; // 64MB chunks 275 | const targetPath = `${path}/${file.name}`; 276 | // Use chunked upload for files larger than 150MB 277 | if (file.size > 150 * 1024 * 1024) { 278 | // Start upload session 279 | const firstChunk = file.slice(0, CHUNK_SIZE); 280 | const sessionStart = await dropboxClient.filesUploadSessionStart({ 281 | close: false, 282 | contents: firstChunk 283 | }); 284 | 285 | let offset = firstChunk.size; 286 | const sessionId = sessionStart.result.session_id; 287 | 288 | // Upload the remaining chunks 289 | while (offset < file.size) { 290 | const chunk = file.slice(offset, offset + CHUNK_SIZE); 291 | const cursor = { 292 | session_id: sessionId, 293 | offset: offset 294 | }; 295 | 296 | // If this is the last chunk, finish the session 297 | if (offset + chunk.size >= file.size) { 298 | const commitInfo = { 299 | path: targetPath, 300 | mode: { '.tag': 'add' }, 301 | autorename: true 302 | }; 303 | 304 | const response = await dropboxClient.filesUploadSessionFinish({ 305 | cursor: cursor, 306 | commit: commitInfo, 307 | contents: chunk 308 | }); 309 | return response.result; 310 | } else { 311 | // Upload intermediate chunk 312 | await dropboxClient.filesUploadSessionAppendV2({ 313 | cursor: cursor, 314 | close: false, 315 | contents: chunk 316 | }); 317 | offset += chunk.size; 318 | } 319 | } 320 | } else { 321 | // Use simple upload for small files 322 | const response = await dropboxClient.filesUpload({ 323 | path: targetPath, 324 | contents: file, 325 | mode: { '.tag': 'add' }, 326 | autorename: true, 327 | }); 328 | return response.result; 329 | } 330 | } catch (error) { 331 | console.error('Error uploading file:', error); 332 | throw error; 333 | } 334 | } 335 | 336 | /** 337 | * Gets existing shared link settings for a path 338 | * @param {Dropbox} dropboxClient - Dropbox client instance 339 | * @param {string} path - Path to check 340 | * @returns {Promise} Shared link settings or null if no link exists 341 | */ 342 | export async function getSharedLinkSettings(dropboxClient, path) { 343 | try { 344 | const response = await dropboxClient.sharingListSharedLinks({ 345 | path, 346 | direct_only: true 347 | }); 348 | 349 | const viewerLinks = response.result.links.filter(link => 350 | link.link_permissions.link_access_level['.tag'] === 'viewer' 351 | ); 352 | if (viewerLinks.length > 0) { 353 | const link = viewerLinks[0]; 354 | const settings = {}; 355 | 356 | // Extract link permissions 357 | if (link.link_permissions) { 358 | settings.allow_download = link.link_permissions.allow_download; 359 | settings.require_password = link.link_permissions.require_password; 360 | 361 | // Get audience type 362 | if (link.link_permissions.effective_audience) { 363 | const visibility = link.link_permissions.effective_audience['.tag']; 364 | if (visibility === 'team') settings.audience = 'team'; 365 | else if (visibility === 'public') settings.audience = 'public'; 366 | else if (visibility === 'no_one') settings.audience = 'no_one'; 367 | else if (visibility === 'password') settings.audience = 'password'; 368 | else if (visibility === 'members') settings.audience = 'members'; 369 | else settings.audience = 'public'; // default 370 | } 371 | } 372 | 373 | // Check expiration 374 | if (link.expires) { 375 | settings.expires = 'custom'; 376 | settings.expiration_timestamp = link.expires.split('T')[0]; // Get just the date part 377 | } else { 378 | settings.expires = 'never'; 379 | } 380 | 381 | return { 382 | url: link.url, 383 | settings 384 | }; 385 | } 386 | return null; 387 | } catch (error) { 388 | console.error('Error getting shared link settings:', error); 389 | throw error; 390 | } 391 | } 392 | 393 | /** 394 | * Creates shared link settings object from user input 395 | * @param {Object} settings - User settings 396 | * @returns {Object} Dropbox API compatible settings object 397 | */ 398 | function createLinkSettings(settings) { 399 | const linkSettings = { 400 | access: { '.tag': 'viewer' } // Always set to viewer access 401 | }; 402 | 403 | // Handle download permissions 404 | if (typeof settings.allow_download === 'boolean') { 405 | linkSettings.allow_download = settings.allow_download; 406 | } 407 | 408 | // Set audience (who can access) 409 | switch (settings.audience) { 410 | case 'team': 411 | linkSettings.audience = { '.tag': 'team' }; 412 | break; 413 | case 'no_one': 414 | linkSettings.audience = { '.tag': 'no_one' }; 415 | break; 416 | case 'password': 417 | linkSettings.audience = { '.tag': 'password' }; 418 | break; 419 | case 'members': 420 | linkSettings.audience = { '.tag': 'members' }; 421 | break; 422 | case 'other': 423 | linkSettings.audience = { '.tag': 'other' }; 424 | break; 425 | default: 426 | linkSettings.audience = { '.tag': 'public' }; 427 | } 428 | 429 | // Configure password protection 430 | if (typeof settings.require_password == 'boolean') { 431 | linkSettings.require_password = settings.require_password; 432 | if (settings.require_password) { 433 | if (!settings.link_password) { 434 | throw new Error('Password is required when password protection is enabled'); 435 | } 436 | linkSettings.link_password = settings.link_password; 437 | } else { 438 | linkSettings.require_password = false; 439 | linkSettings.link_password = null; 440 | } 441 | } 442 | 443 | // Handle expiration 444 | if (settings.expires && settings.expires !== 'never') { 445 | let expiryDate; 446 | 447 | if (settings.expires === 'custom' && settings.expiration_timestamp) { 448 | // For custom dates, use the provided timestamp and ensure it's treated as UTC end of day 449 | expiryDate = new Date(`${settings.expiration_timestamp}T23:59:59Z`); 450 | } else { 451 | // For preset options (1, 7, 30 days) 452 | const days = parseInt(settings.expires); 453 | if (!isNaN(days)) { 454 | expiryDate = new Date(); 455 | expiryDate.setUTCDate(expiryDate.getUTCDate() + days); 456 | expiryDate.setUTCHours(23, 59, 59, 999); 457 | } 458 | } 459 | 460 | if (expiryDate && !isNaN(expiryDate.getTime())) { 461 | linkSettings.expires = expiryDate.toISOString().replace(/\.\d{3}Z$/, 'Z'); 462 | } 463 | } 464 | 465 | return linkSettings; 466 | } 467 | 468 | /** 469 | * Creates a new shared link 470 | * @param {Dropbox} dropboxClient - Dropbox client instance 471 | * @param {string} path - Path to share 472 | * @param {Object} settings - Share settings 473 | * @returns {Promise} Share result 474 | */ 475 | export async function createSharedLink(dropboxClient, path, settings) { 476 | const linkSettings = createLinkSettings(settings); 477 | 478 | try { 479 | const response = await dropboxClient.sharingCreateSharedLinkWithSettings({ 480 | path, 481 | settings: linkSettings 482 | }); 483 | return { 484 | url: response.result.url 485 | }; 486 | } catch (error) { 487 | // If the link already exists, try to get it and update its settings 488 | if (error?.error?.['.tag'] === 'shared_link_already_exists') { 489 | const links = await dropboxClient.sharingListSharedLinks({ 490 | path, 491 | direct_only: true 492 | }); 493 | 494 | if (links.result.links.length > 0) { 495 | const existingLink = links.result.links[0]; 496 | return await updateSharedLink(dropboxClient, existingLink.url, settings); 497 | } 498 | } 499 | throw error; 500 | } 501 | } 502 | 503 | /** 504 | * Updates an existing shared link 505 | * @param {Dropbox} dropboxClient - Dropbox client instance 506 | * @param {string} url - Existing shared link URL 507 | * @param {Object} settings - New settings 508 | * @returns {Promise} Update result 509 | */ 510 | export async function updateSharedLink(dropboxClient, url, settings) { 511 | if (!url) { 512 | throw new Error('URL is required to update shared link settings'); 513 | } 514 | 515 | const linkSettings = createLinkSettings(settings); 516 | const response = await dropboxClient.sharingModifySharedLinkSettings({ 517 | url, 518 | settings: linkSettings, 519 | remove_expiration: !settings.expires 520 | }); 521 | return { 522 | url: response.result.url 523 | }; 524 | } 525 | 526 | /** 527 | * Revokes (deletes) a shared link 528 | * @param {Dropbox} dropboxClient - Dropbox client instance 529 | * @param {string} url - URL of the shared link to revoke 530 | * @returns {Promise} Revoke result 531 | */ 532 | export async function revokeSharedLink(dropboxClient, url) { 533 | if (!url) { 534 | throw new Error('URL is required to revoke a shared link'); 535 | } 536 | 537 | await dropboxClient.sharingRevokeSharedLink({ url }); 538 | return { 539 | message: 'Shared link revoked successfully' 540 | }; 541 | } 542 | 543 | /** 544 | * Starts a long polling request to monitor folder changes 545 | * @param {Dropbox} dropboxClient - Dropbox client instance 546 | * @param {string} cursor - The cursor from listFolder or listFolderContinue 547 | * @param {number} timeout - Optional timeout in seconds (default: 30) 548 | * @returns {Promise} Long poll result with changes flag 549 | */ 550 | export async function startLongPoll(cursor, timeout = 30) { 551 | try { 552 | // Create a new Dropbox instance specifically for long polling, because the longpoll endpoint is noAuth 553 | const longpollClient = new Dropbox(); 554 | const response = await longpollClient.filesListFolderLongpoll({ 555 | cursor, 556 | timeout 557 | }); 558 | return { 559 | changes: response.result.changes, 560 | backoff: response.result.backoff, 561 | trigger: response.result.trigger 562 | }; 563 | } catch (error) { 564 | console.error('Long polling error:', error); 565 | throw error; 566 | } 567 | } 568 | 569 | /** 570 | * Gets folder changes since the last cursor 571 | * @param {Dropbox} dropboxClient - Dropbox client instance 572 | * @param {string} cursor - The cursor from previous listing 573 | * @returns {Promise} Changes with entries and new cursor 574 | */ 575 | export async function getChanges(dropboxClient, cursor) { 576 | try { 577 | let allChanges = []; 578 | let response = await dropboxClient.filesListFolderContinue({ cursor }); 579 | allChanges = allChanges.concat(response.result.entries); 580 | 581 | // Handle pagination of changes 582 | while (response.result.has_more) { 583 | response = await dropboxClient.filesListFolderContinue({ 584 | cursor: response.result.cursor 585 | }); 586 | allChanges = allChanges.concat(response.result.entries); 587 | } 588 | 589 | return { 590 | entries: allChanges, 591 | cursor: response.result.cursor 592 | }; 593 | } catch (error) { 594 | console.error('Error getting changes:', error); 595 | throw error; 596 | } 597 | } 598 | 599 | // ============================================================================ 600 | // Utility Functions 601 | // ============================================================================ 602 | 603 | /** 604 | * Formats bytes into human-readable size 605 | * @param {number} bytes - Number of bytes 606 | * @returns {string} Formatted size string 607 | */ 608 | export function formatBytes(bytes) { 609 | if (bytes === 0) return '0 B'; 610 | const k = 1024; 611 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; 612 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 613 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; 614 | } 615 | 616 | /** 617 | * Formats a timestamp into a localized date string 618 | * @param {string|number} timestamp - Timestamp to format 619 | * @returns {string} Formatted date string 620 | */ 621 | export function formatDate(timestamp) { 622 | return new Date(timestamp).toLocaleString(undefined, { 623 | year: 'numeric', 624 | month: '2-digit', 625 | day: '2-digit', 626 | hour: '2-digit', 627 | minute: '2-digit', 628 | hour12: true, 629 | }); 630 | } --------------------------------------------------------------------------------