├── src
├── backend
│ ├── __init__.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── sdk_relay.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── swagger
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_openapi_schema.py
│ │ │ ├── authentication
│ │ │ │ └── __init__.py
│ │ │ ├── external_api
│ │ │ │ └── items
│ │ │ │ │ └── __init__.py
│ │ │ ├── utils
│ │ │ │ └── urls.py
│ │ │ └── test_settings.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ ├── 0013_active_unnaccent_postgres_extension.py
│ │ │ ├── 00006_create_pg_trgm_extension.py
│ │ │ ├── 0005_item_description.py
│ │ │ ├── 0004_item_size.py
│ │ │ ├── 0002_item_mimetype_alter_user_language.py
│ │ │ ├── 0007_item_hard_deleted_at.py
│ │ │ ├── 0003_item_main_workspace_alter_user_language.py
│ │ │ ├── 0012_item_malware_detection_info.py
│ │ │ ├── 0008_alter_item_options_and_more.py
│ │ │ ├── 0009_alter_user_language.py
│ │ │ ├── 0014_alter_user_language.py
│ │ │ ├── 0010_alter_item_upload_state.py
│ │ │ ├── 0011_update_items_upload_state_value.py
│ │ │ └── 0015_user_claims_alter_user_language.py
│ │ ├── templatetags
│ │ │ └── __init__.py
│ │ ├── management
│ │ │ └── commands
│ │ │ │ ├── __init__.py
│ │ │ │ └── update_suspicious_item_file_hash.py
│ │ ├── authentication
│ │ │ ├── exceptions.py
│ │ │ └── views.py
│ │ ├── templates
│ │ │ └── core
│ │ │ │ └── generate_document.html
│ │ ├── entitlements
│ │ │ ├── __init__.py
│ │ │ ├── entitlements_backend.py
│ │ │ └── dummy_entitlements_backend.py
│ │ ├── storage
│ │ │ ├── __init__.py
│ │ │ ├── storage_compute_backend.py
│ │ │ └── creator_storage_compute_backend.py
│ │ ├── enums.py
│ │ ├── apps.py
│ │ ├── api
│ │ │ ├── fields.py
│ │ │ └── __init__.py
│ │ └── signals.py
│ ├── demo
│ │ ├── __init__.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ └── test_commands_create_demo.py
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ └── commands
│ │ │ │ └── __init__.py
│ │ └── defaults.py
│ ├── e2e
│ │ ├── __init__.py
│ │ ├── management
│ │ │ └── commands
│ │ │ │ └── __init__.py
│ │ ├── utils.py
│ │ ├── serializers.py
│ │ ├── urls.py
│ │ └── viewsets.py
│ ├── wopi
│ │ ├── __init__.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ └── lock.py
│ │ ├── tasks
│ │ │ └── __init__.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ ├── services
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_lock.py
│ │ │ ├── tasks
│ │ │ │ └── __init__.py
│ │ │ ├── viewset
│ │ │ │ └── __init__.py
│ │ │ ├── management_commands
│ │ │ │ ├── __init__.py
│ │ │ │ └── test_trigger_wopi_configuration.py
│ │ │ └── conftest.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── management
│ │ │ └── commands
│ │ │ │ ├── __init__.py
│ │ │ │ └── trigger_wopi_configuration.py
│ │ ├── apps.py
│ │ ├── urls.py
│ │ └── permissions.py
│ ├── MANIFEST.in
│ ├── drive
│ │ ├── __init__.py
│ │ ├── wsgi.py
│ │ └── celery_app.py
│ └── manage.py
├── frontend
│ ├── apps
│ │ ├── sdk-consumer
│ │ │ ├── src
│ │ │ │ ├── App.css
│ │ │ │ ├── vite-env.d.ts
│ │ │ │ └── main.tsx
│ │ │ ├── tsconfig.json
│ │ │ ├── vite.config.ts
│ │ │ ├── .gitignore
│ │ │ ├── index.html
│ │ │ ├── tsconfig.node.json
│ │ │ ├── package.json
│ │ │ ├── tsconfig.app.json
│ │ │ ├── eslint.config.js
│ │ │ └── public
│ │ │ │ └── vite.svg
│ │ ├── drive
│ │ │ ├── src
│ │ │ │ ├── features
│ │ │ │ │ ├── explorer
│ │ │ │ │ │ ├── api
│ │ │ │ │ │ │ └── useAccesses.tsx
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── app-view
│ │ │ │ │ │ │ │ ├── ExplorerFilters.scss
│ │ │ │ │ │ │ │ └── ExplorerSearchButton.tsx
│ │ │ │ │ │ │ ├── tree
│ │ │ │ │ │ │ │ ├── ExploreDragOverlay.tsx
│ │ │ │ │ │ │ │ └── nav
│ │ │ │ │ │ │ │ │ ├── ExplorerTreeNav.tsx
│ │ │ │ │ │ │ │ │ └── ExplorerTreeNavItem.tsx
│ │ │ │ │ │ │ ├── embedded-explorer
│ │ │ │ │ │ │ │ ├── hooks.tsx
│ │ │ │ │ │ │ │ ├── EmbeddedExplorerGridUpdatedAtCell.tsx
│ │ │ │ │ │ │ │ └── EmbeddedExplorerGridMobileCell.tsx
│ │ │ │ │ │ │ ├── trash
│ │ │ │ │ │ │ │ └── utils.tsx
│ │ │ │ │ │ │ ├── icons
│ │ │ │ │ │ │ │ └── ItemIcon.scss
│ │ │ │ │ │ │ ├── toasts
│ │ │ │ │ │ │ │ └── addItemsMovedToast.tsx
│ │ │ │ │ │ │ ├── Draggable.tsx
│ │ │ │ │ │ │ ├── modals
│ │ │ │ │ │ │ │ ├── move
│ │ │ │ │ │ │ │ │ └── ExplorerMoveFolderModal.scss
│ │ │ │ │ │ │ │ └── HardDeleteConfirmationModal.tsx
│ │ │ │ │ │ │ ├── Droppable.tsx
│ │ │ │ │ │ │ ├── workspaces-explorer
│ │ │ │ │ │ │ │ └── WorkspacesExplorer.tsx
│ │ │ │ │ │ │ └── item-actions
│ │ │ │ │ │ │ │ └── ImportDropdown.tsx
│ │ │ │ │ │ └── hooks
│ │ │ │ │ │ │ ├── useBreadcrumb.tsx
│ │ │ │ │ │ │ ├── useInfiniteItems.ts
│ │ │ │ │ │ │ ├── useInfiniteChildren.ts
│ │ │ │ │ │ │ ├── useDeleteItem.tsx
│ │ │ │ │ │ │ └── useQueries.tsx
│ │ │ │ │ ├── layouts
│ │ │ │ │ │ └── components
│ │ │ │ │ │ │ ├── explorer
│ │ │ │ │ │ │ └── ExplorerLayout.scss
│ │ │ │ │ │ │ ├── global
│ │ │ │ │ │ │ ├── GlobalLayout.scss
│ │ │ │ │ │ │ └── GlobalLayout.tsx
│ │ │ │ │ │ │ ├── left-panel
│ │ │ │ │ │ │ ├── LeftPanelMobile.scss
│ │ │ │ │ │ │ └── LeftPanelMobile.tsx
│ │ │ │ │ │ │ ├── header
│ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ └── simple
│ │ │ │ │ │ │ └── SimpleLayout.tsx
│ │ │ │ │ ├── errors
│ │ │ │ │ │ └── AppError.ts
│ │ │ │ │ ├── ui
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── infinite-scroll
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ └── InfiniteScroll.scss
│ │ │ │ │ │ │ ├── spinner
│ │ │ │ │ │ │ │ ├── SpinnerPage.scss
│ │ │ │ │ │ │ │ └── SpinnerPage.tsx
│ │ │ │ │ │ │ ├── responsive
│ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ └── ResponsiveDivs.tsx
│ │ │ │ │ │ │ ├── info
│ │ │ │ │ │ │ │ ├── InfoRow.scss
│ │ │ │ │ │ │ │ └── InfoRow.tsx
│ │ │ │ │ │ │ ├── generic-disclaimer
│ │ │ │ │ │ │ │ ├── GenericDisclaimer.scss
│ │ │ │ │ │ │ │ └── GenericDisclaimer.tsx
│ │ │ │ │ │ │ ├── user
│ │ │ │ │ │ │ │ └── UserProfile.tsx
│ │ │ │ │ │ │ ├── gaufre
│ │ │ │ │ │ │ │ └── Gaufre.tsx
│ │ │ │ │ │ │ ├── breadcrumbs
│ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ ├── toaster
│ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ └── icon
│ │ │ │ │ │ │ │ └── Icon.tsx
│ │ │ │ │ │ ├── preview
│ │ │ │ │ │ │ ├── pdf-preview
│ │ │ │ │ │ │ │ ├── pdf-preview.scss
│ │ │ │ │ │ │ │ └── PreviewPdf.tsx
│ │ │ │ │ │ │ ├── wopi
│ │ │ │ │ │ │ │ └── WopiEditor.scss
│ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ ├── controls
│ │ │ │ │ │ │ │ │ └── PreviewControls.scss
│ │ │ │ │ │ │ │ └── duration-bar
│ │ │ │ │ │ │ │ │ └── DurationBar.tsx
│ │ │ │ │ │ │ ├── audio-player
│ │ │ │ │ │ │ │ └── audio-player.scss
│ │ │ │ │ │ │ ├── preview.scss
│ │ │ │ │ │ │ ├── suspicious
│ │ │ │ │ │ │ │ ├── SuspiciousPreview.scss
│ │ │ │ │ │ │ │ └── SuspiciousPreview.tsx
│ │ │ │ │ │ │ ├── error
│ │ │ │ │ │ │ │ └── ErrorPreview.scss
│ │ │ │ │ │ │ └── not-supported
│ │ │ │ │ │ │ │ └── NotSupportedPreview.scss
│ │ │ │ │ │ └── cunningham
│ │ │ │ │ │ │ └── useCunninghamTheme.ts
│ │ │ │ │ ├── users
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── hooks
│ │ │ │ │ │ │ └── useUserQueries.tsx
│ │ │ │ │ ├── i18n
│ │ │ │ │ │ ├── conf.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── items
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ └── ItemInfo.scss
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── drivers
│ │ │ │ │ │ ├── implementations
│ │ │ │ │ │ │ ├── ResanaDriver.ts
│ │ │ │ │ │ │ └── DummyDriver.ts
│ │ │ │ │ │ ├── utils.tsx
│ │ │ │ │ │ └── DTOs
│ │ │ │ │ │ │ ├── InvitationDTO.ts
│ │ │ │ │ │ │ └── AccessesDTO.ts
│ │ │ │ │ ├── config
│ │ │ │ │ │ ├── useApiConfig.ts
│ │ │ │ │ │ ├── Config.ts
│ │ │ │ │ │ └── ConfigProvider.tsx
│ │ │ │ │ ├── auth
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── LogoutButton.tsx
│ │ │ │ │ │ │ └── LoginButton.tsx
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── analytics
│ │ │ │ │ │ └── AnalyticsProvider.tsx
│ │ │ │ │ ├── sdk
│ │ │ │ │ │ └── SdkRelayManager.ts
│ │ │ │ │ └── api
│ │ │ │ │ │ └── utils.ts
│ │ │ │ ├── assets
│ │ │ │ │ ├── grid_empty.png
│ │ │ │ │ ├── home
│ │ │ │ │ │ └── banner.png
│ │ │ │ │ ├── search-dev.png
│ │ │ │ │ ├── empty-selection.png
│ │ │ │ │ ├── mutliple-selection.png
│ │ │ │ │ ├── icons
│ │ │ │ │ │ ├── settings.svg
│ │ │ │ │ │ ├── upload_file.svg
│ │ │ │ │ │ ├── trash.svg
│ │ │ │ │ │ ├── undo.svg
│ │ │ │ │ │ ├── delete_filled.svg
│ │ │ │ │ │ ├── undo_blue.svg
│ │ │ │ │ │ ├── upload_folder.svg
│ │ │ │ │ │ ├── create_folder.svg
│ │ │ │ │ │ ├── cancel.svg
│ │ │ │ │ │ ├── cancel_blue.svg
│ │ │ │ │ │ └── info.svg
│ │ │ │ │ ├── files
│ │ │ │ │ │ └── icons
│ │ │ │ │ │ │ ├── mime-video-mini.svg
│ │ │ │ │ │ │ ├── mime-other-mini.svg
│ │ │ │ │ │ │ ├── mime-folder-mini.svg
│ │ │ │ │ │ │ ├── mime-audio-mini.svg
│ │ │ │ │ │ │ ├── mime-powerpoint-mini.svg
│ │ │ │ │ │ │ └── mime-image-mini.svg
│ │ │ │ │ ├── tree
│ │ │ │ │ │ ├── folder.svg
│ │ │ │ │ │ └── main-workspace.svg
│ │ │ │ │ ├── logo-icon.svg
│ │ │ │ │ ├── folder
│ │ │ │ │ │ └── folder.svg
│ │ │ │ │ └── workspace_logo.svg
│ │ │ │ ├── utils
│ │ │ │ │ ├── useLayout.tsx
│ │ │ │ │ ├── entitlements.ts
│ │ │ │ │ └── useQueries.ts
│ │ │ │ ├── pages
│ │ │ │ │ ├── _document.tsx
│ │ │ │ │ ├── explorer
│ │ │ │ │ │ └── items
│ │ │ │ │ │ │ ├── public.tsx
│ │ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ │ └── [id].tsx
│ │ │ │ │ ├── 401.tsx
│ │ │ │ │ ├── 403.tsx
│ │ │ │ │ └── sdk
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ └── hooks
│ │ │ │ │ ├── useThemeCustomization.tsx
│ │ │ │ │ └── useCopyToClipboard.tsx
│ │ │ ├── .env
│ │ │ ├── __mocks__
│ │ │ │ └── fileMock.js
│ │ │ ├── .env.development
│ │ │ ├── public
│ │ │ │ └── assets
│ │ │ │ │ ├── favicon.png
│ │ │ │ │ ├── 401-background.png
│ │ │ │ │ ├── 403-background.png
│ │ │ │ │ ├── anct_favicon.png
│ │ │ │ │ └── logo-suite-numerique.png
│ │ │ ├── next.config.ts
│ │ │ ├── eslint.config.mjs
│ │ │ ├── svg.d.ts
│ │ │ ├── tsconfig.json
│ │ │ ├── .gitignore
│ │ │ ├── conf
│ │ │ │ └── default.conf
│ │ │ └── jest.config.ts
│ │ └── e2e
│ │ │ ├── __tests__
│ │ │ └── app-drive
│ │ │ │ ├── assets
│ │ │ │ ├── pv_cm.pdf
│ │ │ │ └── test-image.heic
│ │ │ │ ├── db.setup.ts
│ │ │ │ ├── login.spec.ts
│ │ │ │ ├── item
│ │ │ │ └── right-content-info.spec.ts
│ │ │ │ ├── create-folder.spec.ts
│ │ │ │ ├── utils-item.ts
│ │ │ │ ├── utils-explorer.ts
│ │ │ │ └── utils-embedded-grid.ts
│ │ │ ├── .gitignore
│ │ │ ├── package.json
│ │ │ └── tsconfig.json
│ ├── packages
│ │ └── sdk
│ │ │ ├── README.md
│ │ │ ├── src
│ │ │ ├── index.ts
│ │ │ ├── Config.ts
│ │ │ ├── vite-env.d.ts
│ │ │ ├── utils.ts
│ │ │ └── Types.ts
│ │ │ ├── .env.development
│ │ │ ├── .env.production
│ │ │ ├── CHANGELOG.md
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ ├── CONTRIBUTING.md
│ │ │ ├── vite.config.ts
│ │ │ ├── tsconfig.app.json
│ │ │ └── public
│ │ │ └── vite.svg
│ └── package.json
├── helm
│ └── drive
│ │ ├── Chart.yaml
│ │ ├── templates
│ │ ├── theme_customization_file_cm.yaml
│ │ ├── media_svc.yaml
│ │ ├── backend_svc.yaml
│ │ ├── frontend_svc.yaml
│ │ ├── posthog_svc.yaml
│ │ └── posthog_assets_svc.yaml
│ │ └── generate-readme.sh
└── mail
│ ├── html-to-text.config.json
│ ├── bin
│ ├── mjml-to-html
│ └── html-to-plain-text
│ └── package.json
├── docs
├── assets
│ ├── drive-UI.png
│ └── banner-drive.png
├── examples
│ └── helm
│ │ ├── redis.values.yaml
│ │ ├── postgresql.values.yaml
│ │ ├── keycloak.values.yaml
│ │ └── minio.values.yaml
├── architecture.md
├── installation
│ └── README.md
└── theming.md
├── cron.json
├── env.d
└── development
│ ├── crowdin
│ ├── postgresql
│ ├── kc_postgresql
│ └── postgresql.e2e
├── bin
├── start-kind.sh
├── manage
├── compose
├── pytest
├── fernetkey
├── clear_db_e2e.sql
├── scalingo_postcompile
├── update_openapi_schema
├── scalingo_postfrontend
├── scalingo_run_web
├── postgres_e2e
├── update_app_cacert.sh
├── pylint
└── scalingo_pgdump.sh
├── .github
├── PULL_REQUEST_TEMPLATE.md
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── Bug_report.md
│ ├── Support_question.md
│ └── Feature_request.md
└── workflows
│ ├── helmfile-linter.yaml
│ ├── release-helm-chart.yaml
│ └── front-dependencies-installation.yml
├── Procfile
├── docker
├── files
│ ├── etc
│ │ └── nginx
│ │ │ └── conf.d
│ │ │ └── default.conf
│ └── usr
│ │ └── local
│ │ ├── etc
│ │ └── gunicorn
│ │ │ └── drive.py
│ │ └── bin
│ │ └── entrypoint
└── onlyoffice
│ ├── log4js
│ └── production.json
│ └── local-development.json
├── .dockerignore
├── renovate.json
├── LICENSE
├── .gitignore
├── SECURITY.md
└── gitlint
└── gitlint_emoji.py
/src/backend/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/demo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/e2e/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/demo/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tasks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/tests/swagger/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/demo/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/services/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/tasks/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/viewset/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/demo/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/e2e/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/tests/authentication/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/core/tests/external_api/items/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/management_commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/api/useAccesses.tsx:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_S3_DOMAIN_REPLACE=
2 | NEXT_PUBLIC_API_ORIGIN=
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/explorer/ExplorerLayout.scss:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = "test-file-stub";
2 |
3 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/backend/wopi/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 | """Management commands for the wopi app."""
2 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/errors/AppError.ts:
--------------------------------------------------------------------------------
1 | export class AppError extends Error {}
2 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # 🗂️ Drive SDK
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/assets/drive-UI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/docs/assets/drive-UI.png
--------------------------------------------------------------------------------
/docs/assets/banner-drive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/docs/assets/banner-drive.png
--------------------------------------------------------------------------------
/cron.json:
--------------------------------------------------------------------------------
1 | {
2 | "jobs": [
3 | {
4 | "command": "0 0 * * * bin/scalingo_pgdump.sh"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Config";
2 | export * from "./Types";
3 | export * from "./Picker";
4 |
--------------------------------------------------------------------------------
/src/helm/drive/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | type: application
3 | name: drive
4 | version: 0.10.1
5 | appVersion: latest
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/infinite-scroll/index.ts:
--------------------------------------------------------------------------------
1 | export { InfiniteScroll } from "./InfiniteScroll";
2 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/pdf-preview/pdf-preview.scss:
--------------------------------------------------------------------------------
1 | .pdf-container__iframe {
2 | border: none;
3 | }
4 |
--------------------------------------------------------------------------------
/env.d/development/crowdin:
--------------------------------------------------------------------------------
1 | CROWDIN_PERSONAL_TOKEN=Your-Personal-Token
2 | CROWDIN_PROJECT_ID=Your-Project-Id
3 | CROWDIN_BASE_PATH=/app/src
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/.env.development:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_S3_DOMAIN_REPLACE=http://localhost:9000
2 | NEXT_PUBLIC_API_ORIGIN=http://localhost:8071
3 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/users/types.ts:
--------------------------------------------------------------------------------
1 | export type User = {
2 | id: string;
3 | name: string;
4 | email?: string;
5 |
6 | };
7 |
--------------------------------------------------------------------------------
/bin/start-kind.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | curl https://raw.githubusercontent.com/numerique-gouv/tools/refs/heads/main/kind/create_cluster.sh | bash -s -- drive
3 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/public/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/public/assets/favicon.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/grid_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/src/assets/grid_empty.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/home/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/src/assets/home/banner.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/search-dev.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/src/assets/search-dev.png
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/.env.development:
--------------------------------------------------------------------------------
1 | VITE_SDK_URL=http://localhost:3000/sdk
2 | VITE_SDK_API_URL=http://localhost:8071/api/v1.0
3 | VITE_SDK_DEBUG=True
--------------------------------------------------------------------------------
/bin/manage:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # shellcheck source=bin/_config.sh
4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
5 |
6 | _django_manage "$@"
7 |
--------------------------------------------------------------------------------
/bin/compose:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # shellcheck source=bin/_config.sh
4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
5 |
6 | _docker_compose "$@"
7 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/public/assets/401-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/public/assets/401-background.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/public/assets/403-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/public/assets/403-background.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/public/assets/anct_favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/public/assets/anct_favicon.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/empty-selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/src/assets/empty-selection.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/i18n/conf.ts:
--------------------------------------------------------------------------------
1 | export const LANGUAGES_ALLOWED = ["en-us", "fr-fr"];
2 | export const LANGUAGE_LOCAL_STORAGE = "main-language";
3 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/mutliple-selection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/src/assets/mutliple-selection.png
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/wopi/WopiEditor.scss:
--------------------------------------------------------------------------------
1 | .wopi-editor-iframe {
2 | width: 100%;
3 | height: calc(100vh - 52px);
4 | border: none;
5 | }
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/assets/pv_cm.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/e2e/__tests__/app-drive/assets/pv_cm.pdf
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/src/Config.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_CONFIG = {
2 | url: import.meta.env.VITE_SDK_URL,
3 | apiUrl: import.meta.env.VITE_SDK_API_URL,
4 | };
5 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | Description...
4 |
5 |
6 | ## Proposal
7 |
8 | Description...
9 |
10 | - [] item 1...
11 | - [] item 2...
12 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/constants.ts:
--------------------------------------------------------------------------------
1 | export enum WorkspaceCategory {
2 | SHARED_SPACE = "SHARED_SPACE",
3 | PUBLIC_SPACE = "PUBLIC_SPACE",
4 | }
5 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/public/assets/logo-suite-numerique.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/drive/public/assets/logo-suite-numerique.png
--------------------------------------------------------------------------------
/docs/examples/helm/redis.values.yaml:
--------------------------------------------------------------------------------
1 | redis:
2 | enabled: true
3 | name: redis
4 | #serviceNameOverride: redis
5 | image: redis:8.2-alpine
6 | username: user
7 | password: pass
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/assets/test-image.heic:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/suitenumerique/drive/HEAD/src/frontend/apps/e2e/__tests__/app-drive/assets/test-image.heic
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/.env.production:
--------------------------------------------------------------------------------
1 | VITE_SDK_URL=https://fichiers.suite.anct.gouv.fr/sdk
2 | VITE_SDK_API_URL=https://fichiers.suite.anct.gouv.fr/api/v1.0
3 | VITE_SDK_DEBUG=False
--------------------------------------------------------------------------------
/bin/pytest:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
4 |
5 | _dc_run \
6 | -e DJANGO_CONFIGURATION=Test \
7 | app-dev \
8 | pytest "$@"
9 |
--------------------------------------------------------------------------------
/src/backend/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 | recursive-include src/backend/drive *.html *.png *.gif *.css *.ico *.jpg *.jpeg *.po *.mo *.eot *.svg *.ttf *.woff *.woff2
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerFilters.scss:
--------------------------------------------------------------------------------
1 | .explorer__filters__item {
2 | display: flex;
3 | align-items: center;
4 | gap: 0.5em;
5 | }
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @gouvfr-lasuite/drive-sdk
2 |
3 | ## 0.0.1
4 |
5 | ### Major Changes
6 |
7 | - First version including the File Picker via `openPicker` function.
8 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: bin/scalingo_run_web
2 | worker: celery -A drive.celery_app worker --task-events --beat -l INFO -c $DJANGO_CELERY_CONCURRENCY -Q celery,default
3 | postdeploy: python manage.py migrate
4 |
--------------------------------------------------------------------------------
/src/backend/drive/__init__.py:
--------------------------------------------------------------------------------
1 | """Drive package. Import the celery app early to load shared task form dependencies."""
2 |
3 | from .celery_app import app as celery_app
4 |
5 | __all__ = ["celery_app"]
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/spinner/SpinnerPage.scss:
--------------------------------------------------------------------------------
1 | .drive__spinner-page {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: 100vh;
6 | }
7 |
--------------------------------------------------------------------------------
/src/backend/core/authentication/exceptions.py:
--------------------------------------------------------------------------------
1 | """Exceptions for the authentication module."""
2 |
3 |
4 | class UserCannotAccessApp(Exception):
5 | """Exception raised when a user cannot access the app."""
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Playwright
3 | node_modules/
4 | /test-results/
5 | /playwright-report/
6 | /blob-report/
7 | /playwright/.cache/
8 | /playwright/.auth/
9 | report/
10 | screenshots/
--------------------------------------------------------------------------------
/src/mail/html-to-text.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "wordwrap": 600,
3 | "selectors": [
4 | {
5 | "selector": "h1",
6 | "options": {
7 | "uppercase": false
8 | }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/src/backend/wopi/apps.py:
--------------------------------------------------------------------------------
1 | """Wopi app configuration."""
2 |
3 | from django.apps import AppConfig
4 |
5 |
6 | class WopiConfig(AppConfig):
7 | """Configuration class for the wopi app."""
8 |
9 | name = "wopi"
10 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/utils/useLayout.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | export const useIsMinimalLayout = () => {
4 | const router = useRouter();
5 | return router.query.minimal === "true";
6 | };
7 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/vite.config.ts:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/examples/helm/postgresql.values.yaml:
--------------------------------------------------------------------------------
1 | postgres:
2 | enabled: true
3 | name: postgres
4 | #serviceNameOverride: postgres
5 | image: postgres:16-alpine
6 | username: dinum
7 | password: pass
8 | database: dinum
9 | size: 1Gi
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/utils/entitlements.ts:
--------------------------------------------------------------------------------
1 | import { getDriver } from "@/features/config/Config";
2 |
3 | export const getEntitlements = async () => {
4 | const driver = getDriver();
5 | return driver.getEntitlements();
6 | };
7 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/db.setup.ts:
--------------------------------------------------------------------------------
1 | import { test as setup } from "@playwright/test";
2 |
3 | import { clearDb } from "./utils-common";
4 |
5 | setup("clear the database", async () => {
6 | await clearDb();
7 | });
8 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | interface ImportMetaEnv {
4 | readonly VITE_SDK_URL: string;
5 | }
6 |
7 | interface ImportMeta {
8 | readonly env: ImportMetaEnv;
9 | }
10 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/items/components/ItemInfo.scss:
--------------------------------------------------------------------------------
1 | .item-info {
2 | display: flex;
3 | flex-direction: column;
4 | gap: var(--c--globals--spacings--xs, 0.5rem);
5 | padding: var(--c--globals--spacings--base, 1rem);
6 | }
7 |
--------------------------------------------------------------------------------
/bin/fernetkey:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # shellcheck source=bin/_config.sh
4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
5 |
6 | _dc_run app-dev python -c 'from cryptography.fernet import Fernet;import sys; sys.stdout.write("\n" + Fernet.generate_key().decode() + "\n");'
7 |
--------------------------------------------------------------------------------
/src/backend/demo/defaults.py:
--------------------------------------------------------------------------------
1 | """Parameters that define how the demo site will be built."""
2 |
3 | NB_OBJECTS = {"users": 50, "files": 50, "max_users_per_document": 50}
4 |
5 | DEV_USERS = [
6 | {"username": "drive", "email": "drive@drive.world", "language": "en-us"},
7 | ]
8 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | output: "export",
5 | debug: process.env.NODE_ENV === "development",
6 | reactStrictMode: false,
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/env.d/development/postgresql:
--------------------------------------------------------------------------------
1 | # Postgresql db container configuration
2 | POSTGRES_DB=drive
3 | POSTGRES_USER=dinum
4 | POSTGRES_PASSWORD=pass
5 |
6 | # App database configuration
7 | DB_HOST=postgresql
8 | DB_NAME=drive
9 | DB_USER=dinum
10 | DB_PASSWORD=pass
11 | DB_PORT=5432
12 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/global/GlobalLayout.scss:
--------------------------------------------------------------------------------
1 | nav {
2 | width: 100%;
3 | height: 52px;
4 | background-color: #fff;
5 | border: 1px grey solid;
6 | display: flex;
7 | justify-content: space-around;
8 | align-items: center;
9 | }
10 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/drivers/implementations/ResanaDriver.ts:
--------------------------------------------------------------------------------
1 | // import { Driver } from "../Driver";
2 | // import { Item } from "../types";
3 |
4 | // export class ResanaDriver extends Driver {
5 | // async getItems(): Promise- {
6 | // return [];
7 | // }
8 | // }
9 |
--------------------------------------------------------------------------------
/env.d/development/kc_postgresql:
--------------------------------------------------------------------------------
1 | # Postgresql db container configuration
2 | POSTGRES_DB=keycloak
3 | POSTGRES_USER=drive
4 | POSTGRES_PASSWORD=pass
5 |
6 | # App database configuration
7 | DB_HOST=kc_postgresql
8 | DB_NAME=keycloak
9 | DB_USER=drive
10 | DB_PASSWORD=pass
11 | DB_PORT=5433
12 |
--------------------------------------------------------------------------------
/env.d/development/postgresql.e2e:
--------------------------------------------------------------------------------
1 | # Postgresql db container configuration
2 | POSTGRES_DB=drive_e2e
3 | POSTGRES_USER=dinum
4 | POSTGRES_PASSWORD=pass
5 |
6 | # App database configuration
7 | DB_HOST=postgresql
8 | DB_NAME=drive_e2e
9 | DB_USER=dinum
10 | DB_PASSWORD=pass
11 | DB_PORT=5432
12 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/spinner/SpinnerPage.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from "@gouvfr-lasuite/ui-kit";
2 |
3 | export const SpinnerPage = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/mail/bin/mjml-to-html:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Run mjml command to convert all mjml templates to html files
4 | DIR_MAILS="../backend/core/templates/mail/html/"
5 |
6 | if [ ! -d "${DIR_MAILS}" ]; then
7 | mkdir -p "${DIR_MAILS}";
8 | fi
9 | mjml mjml/*.mjml -o "${DIR_MAILS}";
10 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/drivers/utils.tsx:
--------------------------------------------------------------------------------
1 | import { Item, ItemType } from "./types";
2 |
3 | export const itemIsWorkspace = (item: Item) => {
4 | if (item.main_workspace) {
5 | return false;
6 | }
7 | return item.type === ItemType.FOLDER && item.path.split(".").length === 1;
8 | };
9 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/utils/useQueries.ts:
--------------------------------------------------------------------------------
1 | import { UseQueryOptions } from "@tanstack/react-query";
2 |
3 |
4 | export type HookUseQueryOptions2 = {
5 | enabled?: boolean;
6 | }
7 |
8 |
9 |
10 |
11 | export type HookUseQueryOptions = Omit, "queryKey" | "queryFn">;
12 |
13 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/settings.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/infinite-scroll/InfiniteScroll.scss:
--------------------------------------------------------------------------------
1 | .infinite-scroll {
2 | &__loading-component {
3 | display: flex;
4 | justify-content: center;
5 | padding: 10px;
6 | }
7 |
8 | &__trigger {
9 | min-height: 20px;
10 | margin-top: 10px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 |
6 | createRoot(document.getElementById('root')!).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Fixtures for tests in the drive wopi application"""
2 |
3 | from django.core.cache import cache
4 |
5 | import pytest
6 |
7 |
8 | @pytest.fixture(autouse=True)
9 | def clear_cache():
10 | """Fixture to clear the cache before each test."""
11 | yield
12 | cache.clear()
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/global/GlobalLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Auth } from "@/features/auth/Auth";
2 |
3 | /**
4 | * This layout is used for the global contexts (auth, etc).
5 | */
6 | export const GlobalLayout = ({ children }: { children: React.ReactNode }) => {
7 | return {children};
8 | };
9 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/items/utils.ts:
--------------------------------------------------------------------------------
1 | export const downloadFile = async (url: string, title: string) => {
2 | const a = document.createElement("a");
3 | a.style.display = "none";
4 | a.href = url;
5 | a.download = title;
6 | document.body.appendChild(a);
7 | a.click();
8 | document.body.removeChild(a);
9 | };
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/responsive/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "@/styles/cunningham-tokens-sass" as *;
3 |
4 | $tablet: map.get($themes, "default", "globals", "breakpoints", "tablet");
5 |
6 | #responsive-tablet {
7 | display: none;
8 |
9 | @media (max-width: $tablet) {
10 | display: block;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/bin/clear_db_e2e.sql:
--------------------------------------------------------------------------------
1 | DO $$
2 | DECLARE r RECORD;
3 | BEGIN
4 | FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename != 'django_migrations')
5 | LOOP
6 | RAISE NOTICE 'Truncating table %', r.tablename;
7 | EXECUTE 'TRUNCATE TABLE ' || quote_ident(r.tablename) || ' CASCADE';
8 | END LOOP;
9 | END $$;
10 |
--------------------------------------------------------------------------------
/src/backend/e2e/utils.py:
--------------------------------------------------------------------------------
1 | """E2E utils."""
2 |
3 | from core import factories, models
4 |
5 |
6 | def get_or_create_e2e_user(email):
7 | """Get or create an E2E user."""
8 | user = models.User.objects.filter(email=email).first()
9 | if not user:
10 | user = factories.UserFactory(email=email, sub=None, language="en-us")
11 | return user
12 |
--------------------------------------------------------------------------------
/bin/scalingo_postcompile:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit # always exit on error
4 | set -o pipefail # don't ignore exit codes when piping output
5 |
6 | echo "-----> Running post-compile script"
7 |
8 | # Remove all the files we don't need
9 | rm -rf src docker env.d .cursor .github compose.yaml README.md .cache
10 |
11 | chmod +x bin/scalingo_run_web
12 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/config/useApiConfig.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { getDriver } from "./Config";
3 |
4 | export function useApiConfig() {
5 | const driver = getDriver();
6 | return useQuery({
7 | queryKey: ["config"],
8 | queryFn: () => driver.getConfig(),
9 | staleTime: 1000,
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/pdf-preview/PreviewPdf.tsx:
--------------------------------------------------------------------------------
1 | interface PreviewPdfProps {
2 | src?: string;
3 | }
4 |
5 | export const PreviewPdf = ({ src }: PreviewPdfProps) => {
6 | return (
7 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/theme_customization_file_cm.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.backend.themeCustomization.enabled }}
2 | apiVersion: v1
3 | kind: ConfigMap
4 | metadata:
5 | name: drive-theme-customization
6 | namespace: {{ .Release.Namespace }}
7 | data:
8 | default.json: |
9 | {{ .Values.backend.themeCustomization.file_content | toJson | indent 4 }}
10 | {{- end }}
--------------------------------------------------------------------------------
/bin/update_openapi_schema:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
4 |
5 | _dc_run \
6 | -e DJANGO_CONFIGURATION=Test \
7 | app-dev \
8 | python manage.py spectacular \
9 | --api-version 'v1.0' \
10 | --urlconf 'drive.api_urls' \
11 | --format openapi-json \
12 | --file /app/core/tests/swagger/swagger.json
13 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/components/controls/PreviewControls.scss:
--------------------------------------------------------------------------------
1 | .suite-preview-controls {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | gap: 8px;
6 | }
7 |
8 | .controls-vertical-separator {
9 | height: 24px;
10 | width: 1px;
11 | background-color: var(--c--contextuals--border--surface--primary);
12 | }
13 |
--------------------------------------------------------------------------------
/docker/files/etc/nginx/conf.d/default.conf:
--------------------------------------------------------------------------------
1 |
2 | server {
3 | listen 8083;
4 | server_name localhost;
5 | charset utf-8;
6 |
7 | location / {
8 | proxy_pass http://keycloak:8080;
9 | proxy_set_header Host $host;
10 | proxy_set_header X-Real-IP $remote_addr;
11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/backend/core/templates/core/generate_document.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Generate item
5 |
6 |
7 | Generate item
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/.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 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0013_active_unnaccent_postgres_extension.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations
2 |
3 | from django.contrib.postgres.operations import UnaccentExtension
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0012_item_malware_detection_info'),
10 | ]
11 |
12 | operations = [
13 | UnaccentExtension(),
14 | ]
15 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function randomToken(length: number = 32) {
2 | var result = "";
3 | var characters =
4 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
5 | var charactersLength = characters.length;
6 | for (var i = 0; i < length; i++) {
7 | result += characters.charAt(Math.floor(Math.random() * charactersLength));
8 | }
9 | return result;
10 | }
11 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | ## Architecture
2 |
3 | ### Global system architecture
4 |
5 | ```mermaid
6 | flowchart TD
7 | User -- HTTP --> Front("Frontend (NextJS SPA)")
8 | Front -- REST API --> Back("Backend (Django)")
9 | Front -- OIDC --> Back -- OIDC ---> OIDC("Keycloak / ProConnect")
10 | Back --> DB("Database (PostgreSQL)")
11 | Back <--> Celery --> DB
12 | Back ----> S3-Compatible
13 | ```
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/tree/ExploreDragOverlay.tsx:
--------------------------------------------------------------------------------
1 | type ExplorerDragOverlayProps = {
2 | count: number;
3 | };
4 |
5 | export const ExplorerDragOverlay = ({ count }: ExplorerDragOverlayProps) => {
6 | const filesCount = count > 0 ? count : 1;
7 | return (
8 |
9 | {filesCount} fichiers sélectionnés
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/responsive/ResponsiveDivs.tsx:
--------------------------------------------------------------------------------
1 | export const ResponsiveDivs = () => {
2 | return (
3 | <>
4 |
5 | >
6 | );
7 | };
8 |
9 | export const isTablet = () => {
10 | return (
11 | getComputedStyle(
12 | document.querySelector("#responsive-tablet")!
13 | ).getPropertyValue("display") === "block"
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/00006_create_pg_trgm_extension.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations
2 |
3 |
4 | class Migration(migrations.Migration):
5 | dependencies = [
6 | ("core", "0005_item_description"),
7 | ]
8 |
9 | operations = [
10 | migrations.RunSQL(
11 | "CREATE EXTENSION IF NOT EXISTS pg_trgm;",
12 | reverse_sql="DROP EXTENSION IF EXISTS pg_trgm;",
13 | ),
14 | ]
--------------------------------------------------------------------------------
/src/backend/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """
3 | drive's management script.
4 | """
5 |
6 | import os
7 | import sys
8 |
9 | if __name__ == "__main__":
10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
11 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
12 |
13 | from configurations.management import execute_from_command_line
14 |
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/hooks/useBreadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import { getDriver } from "@/features/config/Config";
2 | import { useQuery } from "@tanstack/react-query";
3 |
4 | export const useBreadcrumbQuery = (id?: string | null) => {
5 | const driver = getDriver();
6 | return useQuery({
7 | queryKey: ["breadcrumb", id],
8 | queryFn: () => driver.getItemBreadcrumb(id!),
9 | enabled: !!id,
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/bin/scalingo_postfrontend:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit # always exit on error
4 | set -o pipefail # don't ignore exit codes when piping output
5 |
6 | echo "-----> Running post-frontend script"
7 |
8 | # Move the frontend build to the nginx root and clean up
9 | mkdir -p build/
10 | mv src/frontend/apps/drive/out build/frontend-out
11 |
12 | mv src/backend/* ./
13 | mv src/nginx/* ./
14 |
15 | echo "3.13" > .python-version
16 |
--------------------------------------------------------------------------------
/docker/onlyoffice/log4js/production.json:
--------------------------------------------------------------------------------
1 | {
2 | "appenders": {
3 | "default": {
4 | "type": "console",
5 | "layout": {
6 | "type": "pattern",
7 | "pattern": "[%d] [%p] %c - %.10000m"
8 | }
9 | }
10 | },
11 | "categories": {
12 | "default": {
13 | "appenders": [
14 | "default"
15 | ],
16 | "level": "DEBUG"
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/backend/core/entitlements/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Entitlements backend utilities.
3 | """
4 |
5 | import functools
6 |
7 | from django.conf import settings
8 | from django.utils.module_loading import import_string
9 |
10 |
11 | @functools.cache
12 | def get_entitlements_backend():
13 | """
14 | Get the entitlements backend.
15 | """
16 | backend = import_string(settings.ENTITLEMENTS_BACKEND)()
17 | return backend
18 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/auth/components/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@openfun/cunningham-react";
2 | import { logout } from "../Auth";
3 | import { useTranslation } from "react-i18next";
4 |
5 | export const LogoutButton = () => {
6 | const { t } = useTranslation();
7 | return (
8 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/backend/core/storage/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Storage compute backend utilities.
3 | """
4 |
5 | import functools
6 |
7 | from django.conf import settings
8 | from django.utils.module_loading import import_string
9 |
10 |
11 | @functools.cache
12 | def get_storage_compute_backend():
13 | """
14 | Get the storage compute backend.
15 | """
16 | backend = import_string(settings.STORAGE_COMPUTE_BACKEND)()
17 | return backend
18 |
--------------------------------------------------------------------------------
/docker/files/usr/local/etc/gunicorn/drive.py:
--------------------------------------------------------------------------------
1 | # Gunicorn-django settings
2 | bind = ["0.0.0.0:8000"]
3 | name = "drive"
4 | python_path = "/app"
5 |
6 | # Run
7 | graceful_timeout = 90
8 | timeout = 90
9 | workers = 3
10 |
11 | # Logging
12 | # Using '-' for the access log file makes gunicorn log accesses to stdout
13 | accesslog = "-"
14 | # Using '-' for the error log file makes gunicorn log errors to stderr
15 | errorlog = "-"
16 | loglevel = "info"
17 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/drivers/DTOs/InvitationDTO.ts:
--------------------------------------------------------------------------------
1 | import { Role } from "../types";
2 |
3 |
4 | export type DTOCreateInvitation = {
5 | itemId: string;
6 | email: string;
7 | role: Role;
8 | };
9 |
10 |
11 | export type DTOUpdateInvitation = {
12 | itemId: string;
13 | invitationId: string;
14 | role: Role;
15 | };
16 |
17 |
18 | export type DTODeleteInvitation = {
19 | itemId: string;
20 | invitationId: string;
21 | };
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/drivers/DTOs/AccessesDTO.ts:
--------------------------------------------------------------------------------
1 | import { Role } from "../types";
2 |
3 | export type DTOCreateAccess = {
4 | itemId: string;
5 | userId: string;
6 | role: Role;
7 | };
8 |
9 |
10 | export type DTOUpdateAccess = {
11 | itemId: string;
12 | accessId: string;
13 | user_id: string;
14 | role: Role;
15 | };
16 |
17 | export type DTODeleteAccess = {
18 | itemId: string;
19 | accessId: string;
20 | }
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/auth/types.ts:
--------------------------------------------------------------------------------
1 | import { Item } from "../drivers/types";
2 |
3 | /**
4 | * Represents user retrieved from the API.
5 | * @interface User
6 | * @property {string} id - The id of the user.
7 | * @property {string} email - The email of the user.
8 | * @property {string} name - The name of the user.
9 | */
10 | export interface User {
11 | id: string;
12 | email: string;
13 | language: string;
14 | main_workspace: Item;
15 | }
16 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/i18n/utils.ts:
--------------------------------------------------------------------------------
1 | export const splitLocaleCode = (language: string) => {
2 | const locale = language.split(/[-_]/);
3 | return {
4 | language: locale[0],
5 | region: locale.length === 2 ? locale[1] : undefined,
6 | };
7 | };
8 |
9 | export const capitalizeRegion = (language: string) => {
10 | const { language: lang, region } = splitLocaleCode(language);
11 | return lang + (region ? "-" + region.toUpperCase() : "");
12 | };
13 |
--------------------------------------------------------------------------------
/docker/onlyoffice/local-development.json:
--------------------------------------------------------------------------------
1 | {
2 | "wopi": {
3 | "enable": true,
4 | "host": "http://localhost:9981",
5 | "pdfView": [],
6 | "pdfEdit": [],
7 | "forms": [],
8 | "wordView": [],
9 | "wordEdit": ["docx", "dotx", "docm"],
10 | "cellView": [],
11 | "cellEdit": ["xlsx", "xlsb", "xltx", "xlsm", "csv"],
12 | "slideView": [],
13 | "slideEdit": ["pptx", "potx", "pptm"],
14 | "diagramView": [],
15 | "diagramEdit": []
16 | }
17 | }
--------------------------------------------------------------------------------
/src/backend/e2e/serializers.py:
--------------------------------------------------------------------------------
1 | """Serializers for E2E tests."""
2 |
3 | from rest_framework import serializers
4 |
5 |
6 | # Suppress the warning about not implementing `create` and `update` methods
7 | # since we don't use a model and only rely on the serializer for validation
8 | # pylint: disable=abstract-method
9 | class E2EAuthSerializer(serializers.Serializer):
10 | """Serializer for E2E authentication."""
11 |
12 | email = serializers.EmailField(required=True)
13 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e2e",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "lint": "eslint . --ext .ts",
7 | "install-playwright": "playwright install --with-deps",
8 | "test": "playwright test"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "description": "",
14 | "devDependencies": {
15 | "@playwright/test": "1.56.1",
16 | "@types/node": "24.10.1"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 | *.pyc
4 | **/__pycache__
5 | **/*.pyc
6 | venv
7 | .venv
8 |
9 | # System-specific files
10 | .DS_Store
11 | **/.DS_Store
12 |
13 | # Docker
14 | compose.*
15 | env.d
16 |
17 | # Docs
18 | docs
19 | *.md
20 | *.log
21 |
22 | # Development/test cache & configurations
23 | data
24 | .cache
25 | .circleci
26 | .git
27 | .vscode
28 | .iml
29 | .idea
30 | db.sqlite3
31 | .mypy_cache
32 | .pylint.d
33 | .pytest_cache
34 |
35 | # Frontend
36 | node_modules
37 |
--------------------------------------------------------------------------------
/bin/scalingo_run_web:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Trigger collabora configuration
4 | python manage.py trigger_wopi_configuration
5 |
6 | # Start the Django backend
7 | gunicorn -b :8000 drive.wsgi:application --log-file - &
8 |
9 | # Start the Nginx server
10 | bin/run &
11 |
12 | # if the current shell is killed, also terminate all its children
13 | trap "pkill SIGTERM -P $$" SIGTERM
14 |
15 | # wait for a single child to finish,
16 | wait -n
17 | # then kill all the other tasks
18 | pkill -P $$
19 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/config/Config.ts:
--------------------------------------------------------------------------------
1 | import { StandardDriver } from "../drivers/implementations/StandardDriver";
2 | // import { DummyDriver } from "../drivers/implementations/DummyDriver";
3 |
4 | export const getConfig = () => {
5 | // TODO: Later, be based on URL query params for instance.
6 | return {
7 | // driver: new DummyDriver(),
8 | driver: new StandardDriver(),
9 | };
10 | };
11 |
12 | export const getDriver = () => {
13 | return getConfig().driver;
14 | };
15 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0005_item_description.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.5 on 2025-04-04 09:33
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0004_item_size'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='description',
16 | field=models.TextField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/embedded-explorer/hooks.tsx:
--------------------------------------------------------------------------------
1 | import { Item } from "@/features/drivers/types";
2 | import { useEmbeddedExplorerGirdContext } from "./EmbeddedExplorerGrid";
3 |
4 | export const useDisableDragGridItem = (item: Item) => {
5 | const { selectedItemsMap, disableItemDragAndDrop } =
6 | useEmbeddedExplorerGirdContext();
7 | const isSelected = !!selectedItemsMap[item.id];
8 | return disableItemDragAndDrop || !isSelected || !item.abilities?.move;
9 | };
10 |
--------------------------------------------------------------------------------
/src/backend/wopi/management/commands/trigger_wopi_configuration.py:
--------------------------------------------------------------------------------
1 | """Management command to trigger wopi configuration celery task."""
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from wopi.tasks.configure_wopi import configure_wopi_clients
6 |
7 |
8 | class Command(BaseCommand):
9 | """Management command to trigger wopi configuration celery task."""
10 |
11 | def handle(self, *args, **options):
12 | """Handle the command."""
13 | configure_wopi_clients.delay()
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/login.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test as setup } from "@playwright/test";
2 |
3 | import { keyCloakSignIn } from "./utils-common";
4 |
5 | setup("authenticate as drive", async ({ page }) => {
6 | await page.goto("/", { waitUntil: "networkidle" });
7 | await page.content();
8 |
9 | await keyCloakSignIn(page, "drive", "drive");
10 |
11 | await expect(
12 | page.getByRole("button", { name: "Open user menu" })
13 | ).toBeVisible({ timeout: 10000 });
14 | });
15 |
--------------------------------------------------------------------------------
/src/helm/drive/generate-readme.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker image ls | grep readme-generator-for-helm
4 | if [ "$?" -ne "0" ]; then
5 | git clone https://github.com/bitnami/readme-generator-for-helm.git /tmp/readme-generator-for-helm
6 | cd /tmp/readme-generator-for-helm
7 | docker build -t readme-generator-for-helm:latest .
8 | cd $(dirname -- "${BASH_SOURCE[0]}")
9 | fi
10 | docker run --rm -it -v .:/source -w /source readme-generator-for-helm:latest readme-generator -v values.yaml -r README.md
11 |
--------------------------------------------------------------------------------
/src/backend/core/enums.py:
--------------------------------------------------------------------------------
1 | """
2 | Core application enums declaration
3 | """
4 |
5 | from django.conf import global_settings
6 | from django.utils.translation import gettext_lazy as _
7 |
8 | # In Django's code base, `LANGUAGES` is set by default with all supported languages.
9 | # We can use it for the choice of languages which should not be limited to the few languages
10 | # active in the app.
11 | # pylint: disable=no-member
12 | ALL_LANGUAGES = {language: _(name) for language, name in global_settings.LANGUAGES}
13 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/media_svc.yaml:
--------------------------------------------------------------------------------
1 | {{- $fullName := include "drive.fullname" . -}}
2 | {{- $component := "media" -}}
3 | apiVersion: v1
4 | kind: Service
5 | metadata:
6 | name: {{ $fullName }}-media
7 | namespace: {{ .Release.Namespace | quote }}
8 | labels:
9 | {{- include "drive.common.labels" (list . $component) | nindent 4 }}
10 | annotations:
11 | {{- toYaml $.Values.serviceMedia.annotations | nindent 4 }}
12 | spec:
13 | type: ExternalName
14 | externalName: {{ $.Values.serviceMedia.host }}
15 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0004_item_size.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.5 on 2025-04-02 08:33
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0003_item_main_workspace_alter_user_language'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='size',
16 | field=models.BigIntegerField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/backend/wopi/urls.py:
--------------------------------------------------------------------------------
1 | """Urls of the WOPI app."""
2 |
3 | from django.conf import settings
4 | from django.urls import include, path
5 |
6 | from wopi.routers import WopiRouter
7 | from wopi.viewsets import WopiViewSet
8 |
9 | router = WopiRouter()
10 | router.register("files", WopiViewSet, basename="files")
11 |
12 | urlpatterns = [
13 | path(
14 | f"api/{settings.API_VERSION}/wopi/",
15 | include(
16 | [
17 | *router.urls,
18 | ]
19 | ),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/upload_file.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/backend/core/entitlements/entitlements_backend.py:
--------------------------------------------------------------------------------
1 | """
2 | Entitlements Backend.
3 | """
4 |
5 | from abc import ABC, abstractmethod
6 |
7 |
8 | class EntitlementsBackend(ABC):
9 | """Abstract base class for entitlements backends."""
10 |
11 | @abstractmethod
12 | def can_access(self, user):
13 | """
14 | Check if a user can access app.
15 | """
16 |
17 | @abstractmethod
18 | def can_upload(self, user):
19 | """
20 | Check if a user can upload a file.
21 | """
22 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0002_item_mimetype_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.5 on 2025-02-12 11:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='mimetype',
16 | field=models.CharField(blank=True, max_length=255, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0007_item_hard_deleted_at.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.8 on 2025-04-16 08:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '00006_create_pg_trgm_extension'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='hard_deleted_at',
16 | field=models.DateTimeField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/undo.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/left-panel/LeftPanelMobile.scss:
--------------------------------------------------------------------------------
1 | .drive__home__left-panel {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | padding: 0.75rem 0;
6 | gap: 12px;
7 | }
8 |
9 | .drive__home__left-panel__gaufre {
10 | position: absolute;
11 | bottom: 0;
12 | padding: 8px 10px;
13 | border-top: 1px solid var(--c--contextuals--border--surface--primary);
14 | display: flex;
15 | align-items: center;
16 | justify-content: flex-end;
17 | width: 100%;
18 | }
19 |
--------------------------------------------------------------------------------
/src/backend/drive/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for the drive project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from configurations.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
16 |
17 | application = get_wsgi_application()
18 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/delete_filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/undo_blue.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0003_item_main_workspace_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.5 on 2025-03-12 09:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0002_item_mimetype_alter_user_language'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='main_workspace',
16 | field=models.BooleanField(default=False),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/trash/utils.tsx:
--------------------------------------------------------------------------------
1 | import { useModals } from "@openfun/cunningham-react";
2 | import i18n from "@/features/i18n/initI18n";
3 |
4 | export const messageModalTrashNavigate = (
5 | modals: ReturnType
6 | ) => {
7 | modals.messageModal({
8 | title: i18n.t("explorer.trash.navigate.modal.title"),
9 | children: (
10 |
11 | {i18n.t("explorer.trash.navigate.modal.description")}
12 |
13 | ),
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true
16 | },
17 | "include": ["**/*.ts", "**/*.d.ts"],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/bin/postgres_e2e:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # shellcheck source=bin/_config.sh
4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
5 |
6 | # Get database credentials from environment file
7 | ENV_FILE="${REPO_DIR}/env.d/development/postgresql.e2e"
8 | POSTGRES_USER=$(grep POSTGRES_USER "$ENV_FILE" | cut -d'=' -f2)
9 | POSTGRES_DB=$(grep POSTGRES_DB "$ENV_FILE" | cut -d'=' -f2)
10 |
11 | # Execute PostgreSQL command (run as postgres user, not host user)
12 | _docker_compose exec -T postgresql psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 "$@"
13 |
--------------------------------------------------------------------------------
/docs/examples/helm/keycloak.values.yaml:
--------------------------------------------------------------------------------
1 | keycloak:
2 | enabled: true
3 | image: quay.io/keycloak/keycloak:20.0.1
4 | name: keycloak
5 | #serviceNameOverride: keycloak
6 | hostname: drive-keycloak.127.0.0.1.nip.io
7 | username: admin
8 | password: pass
9 | tls:
10 | enabled: true
11 | secretName: drive-tls
12 | db:
13 | username: dinum
14 | password: pass
15 | database: keycloak
16 | size: 1Gi
17 | image: postgres:16-alpine
18 | realm:
19 | name: drive
20 | username: drive
21 | password: drive
22 | email: drive@example.com
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/explorer/items/public.tsx:
--------------------------------------------------------------------------------
1 | import { getGlobalExplorerLayout } from "@/features/layouts/components/explorer/ExplorerLayout";
2 | import { WorkspaceType } from "@/features/drivers/types";
3 | import WorkspacesExplorer from "@/features/explorer/components/workspaces-explorer/WorkspacesExplorer";
4 | export default function PublicPage() {
5 | return (
6 |
10 | );
11 | }
12 |
13 | PublicPage.getLayout = getGlobalExplorerLayout;
14 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/explorer/items/shared.tsx:
--------------------------------------------------------------------------------
1 | import { getGlobalExplorerLayout } from "@/features/layouts/components/explorer/ExplorerLayout";
2 | import { WorkspaceType } from "@/features/drivers/types";
3 | import WorkspacesExplorer from "@/features/explorer/components/workspaces-explorer/WorkspacesExplorer";
4 | export default function SharedPage() {
5 | return (
6 |
10 | );
11 | }
12 |
13 | SharedPage.getLayout = getGlobalExplorerLayout;
14 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Release
4 |
5 | 1. Run `npm run build`
6 |
7 | 2. Update CHANGELOG.md according to the Major / Minor / Patch semver convention.
8 |
9 | 3. Based on semver upgrade the package version in `package.json`.
10 |
11 | 4. Commit the changes and create a PR named "🔖(release) version packages".
12 |
13 | 5. Run `npx @changesets/cli publish`. It will publish the new version of the package to NPM and create a git tag.
14 |
15 | 6. Run `git push origin tag @gouvfr-lasuite/drive-sdk@`
16 |
17 | 7. Tell everyone 🎉 !
18 |
--------------------------------------------------------------------------------
/docs/installation/README.md:
--------------------------------------------------------------------------------
1 | # Installation
2 | If you want to install Drive you've come to the right place.
3 |
4 | For now we only have a documentation to install it on Kubernetes. We will more than happy to improve this documentation with other methods.
5 |
6 | Feel free to make a PR to add ones that are not listed after 🙏
7 |
8 | ## Kubernetes
9 | We (Drive maintainers) are only using the Kubernetes deployment method in production. We can only provide advanced support for this method.
10 | Please follow the instructions laid out [here](/docs/installation/kubernetes.md).
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/backend/core/apps.py:
--------------------------------------------------------------------------------
1 | """Drive Core application"""
2 |
3 | from django.apps import AppConfig
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class CoreConfig(AppConfig):
8 | """Configuration class for the drive core app."""
9 |
10 | name = "core"
11 | app_label = "core"
12 | verbose_name = _("drive core application")
13 |
14 | def ready(self):
15 | """
16 | Import signals when the app is ready.
17 | """
18 | # pylint: disable=import-outside-toplevel, unused-import
19 | from . import signals # noqa: PLC0415
20 |
--------------------------------------------------------------------------------
/src/backend/core/storage/storage_compute_backend.py:
--------------------------------------------------------------------------------
1 | """Storage compute backend for calculating storage usage metrics."""
2 |
3 | from abc import ABC, abstractmethod
4 |
5 |
6 | class StorageComputeBackend(ABC):
7 | """Abstract base class for storage compute backends."""
8 |
9 | @abstractmethod
10 | def compute_storage_used(self, user):
11 | """
12 | Compute the storage used by a user.
13 |
14 | Args:
15 | user: The user instance to compute storage for.
16 |
17 | Returns:
18 | int: The storage used in bytes.
19 | """
20 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/cunningham/useCunninghamTheme.ts:
--------------------------------------------------------------------------------
1 | import { useAppContext } from "@/pages/_app";
2 | import { tokens } from "@/styles/cunningham-tokens";
3 |
4 | export const useCunninghamTheme = () => {
5 | const { theme } = useAppContext();
6 |
7 | return tokens.themes[
8 | theme as keyof typeof tokens.themes
9 | ] as (typeof tokens.themes)["default"];
10 | };
11 |
12 | // Once the cunningham sass generated string is fixed, we can remove this function.
13 | export const removeQuotes = (str: string) => {
14 | return str.replace(/^['"]|['"]$/g, "");
15 | };
16 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/management_commands/test_trigger_wopi_configuration.py:
--------------------------------------------------------------------------------
1 | """Tests for the trigger_wopi_configuration management command."""
2 |
3 | from unittest import mock
4 |
5 | from django.core.management import call_command
6 |
7 | from wopi.tasks.configure_wopi import configure_wopi_clients
8 |
9 |
10 | def test_trigger_wopi_configuration():
11 | """Test the trigger_wopi_configuration management command."""
12 | with mock.patch.object(configure_wopi_clients, "delay") as mock_delay:
13 | call_command("trigger_wopi_configuration")
14 | mock_delay.assert_called_once()
15 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/audio-player/audio-player.scss:
--------------------------------------------------------------------------------
1 | .audio-player {
2 | padding: 16px;
3 | width: 100%;
4 | transition: all 0.3s ease;
5 |
6 | &__container {
7 | display: flex;
8 | flex-direction: column;
9 | gap: 12px;
10 | }
11 |
12 | &__info {
13 | text-align: center;
14 | }
15 |
16 | &__title {
17 | font-size: 16px;
18 | font-weight: 600;
19 | margin-bottom: 4px;
20 | line-height: 1.2;
21 | }
22 | }
23 |
24 | // Responsive design
25 | @media (max-width: 768px) {
26 | .audio-player {
27 | padding: 12px;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/upload_folder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/preview.scss:
--------------------------------------------------------------------------------
1 | @use "./audio-player/audio-player.scss";
2 | @use "./pdf-preview/pdf-preview.scss";
3 | @use "./files-preview/files-preview.scss";
4 | @use "./image-viewer/ImageViewer.scss";
5 | @use "./video-player/VideoPlayer.scss";
6 | @use "./components/duration-bar/DurationBar.scss";
7 | @use "./components/volume-bar/VolumeBar.scss";
8 | @use "./components/controls/PreviewControls.scss";
9 | @use "./not-supported/NotSupportedPreview.scss";
10 | @use "./suspicious/SuspiciousPreview.scss";
11 | @use "./wopi/WopiEditor.scss";
12 | @use "./error/ErrorPreview.scss";
13 |
--------------------------------------------------------------------------------
/docs/examples/helm/minio.values.yaml:
--------------------------------------------------------------------------------
1 | minio:
2 | enabled: true
3 | image: minio/minio
4 | name: minio
5 | # serviceNameOverride: drive-minio
6 | ingress:
7 | enabled: true
8 | hostname: drive-minio.127.0.0.1.nip.io
9 | tls:
10 | enabled: true
11 | secretName: drive-tls
12 | consoleIngress:
13 | enabled: true
14 | hostname: drive-minio-console.127.0.0.1.nip.io
15 | tls:
16 | enabled: true
17 | secretName: drive-tls
18 | api:
19 | port: 80
20 | username: dinum
21 | password: password
22 | bucket: drive-media-storage
23 | versioning: true
24 | size: 1Gi
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/src/Types.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_CONFIG } from "@/Config";
2 |
3 | export type ConfigType = typeof DEFAULT_CONFIG;
4 |
5 | export enum ClientMessageType {
6 | // Picker.
7 | ITEMS_SELECTED = "ITEMS_SELECTED",
8 | CANCEL = "CANCEL",
9 | // Saver
10 | SAVER_READY = "SAVER_READY",
11 | SAVER_PAYLOAD = "SAVER_PAYLOAD",
12 | ITEM_SAVED = "ITEM_SAVED",
13 | }
14 |
15 | export interface SDKRelayEvent {
16 | type: string;
17 | data: any;
18 | }
19 |
20 | export interface Item {
21 | id: string;
22 | title: string;
23 | url: string;
24 | size: number;
25 | type: "file";
26 | }
27 |
--------------------------------------------------------------------------------
/src/backend/core/entitlements/dummy_entitlements_backend.py:
--------------------------------------------------------------------------------
1 | """
2 | Dummy Entitlements Backend.
3 | """
4 |
5 | from core.entitlements.entitlements_backend import EntitlementsBackend
6 |
7 |
8 | class DummyEntitlementsBackend(EntitlementsBackend):
9 | """Dummy entitlements backend for testing purposes."""
10 |
11 | def can_access(self, user):
12 | """
13 | Check if a user can access app.
14 | """
15 | return {"result": True}
16 |
17 | def can_upload(self, user):
18 | """
19 | Check if a user can upload a file.
20 | """
21 | return {"result": True}
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/info/InfoRow.scss:
--------------------------------------------------------------------------------
1 | .info-row {
2 | display: flex;
3 | justify-content: space-between;
4 | padding: var(--c--globals--spacings--3xs, 0.25rem) 0;
5 | align-items: center;
6 |
7 | &__label {
8 | font-size: var(--c--globals--font--sizes--sm);
9 | color: var(--c--contextuals--content--semantic--neutral--primary);
10 | font-weight: 700;
11 | }
12 |
13 | &__right-content {
14 | &__string {
15 | font-size: var(--c--globals--font--sizes--sm);
16 | color: var(--c--contextuals--content--semantic--neutral--secondary);
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "main",
3 | "version": "0.10.1",
4 | "private": true,
5 | "workspaces": [
6 | "apps/*",
7 | "packages/*"
8 | ],
9 | "engines": {
10 | "node": ">=22.0.0 <25.0.0",
11 | "yarn": "1.22.22"
12 | },
13 | "scripts": {
14 | "dev": "yarn workspace drive run dev",
15 | "build": "yarn workspace drive run build",
16 | "lint": "yarn workspace drive run lint",
17 | "build-theme": "yarn workspace drive build-theme"
18 | },
19 | "resolutions": {},
20 | "devDependencies": {
21 | "turbo": "2.6.1"
22 | },
23 | "packageManager": "yarn@1.22.22"
24 | }
25 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0012_item_malware_detection_info.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.5 on 2025-08-28 14:20
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0011_update_items_upload_state_value'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='item',
15 | name='malware_detection_info',
16 | field=models.JSONField(blank=True, default=dict, help_text='Malware detection info when the analysis status is unsafe.', null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/backend/e2e/urls.py:
--------------------------------------------------------------------------------
1 | """URL configuration for the e2e app."""
2 |
3 | from django.conf import settings
4 | from django.urls import include, path
5 |
6 | from rest_framework.routers import DefaultRouter
7 |
8 | from e2e import viewsets
9 |
10 | user_auth_router = DefaultRouter()
11 | user_auth_router.register(
12 | "user-auth",
13 | viewsets.UserAuthViewSet,
14 | basename="user-auth",
15 | )
16 |
17 | urlpatterns = [
18 | path(
19 | f"api/{settings.API_VERSION}/e2e/",
20 | include(
21 | [
22 | *user_auth_router.urls,
23 | ]
24 | ),
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | {
15 | rules: {
16 | "react-hooks/exhaustive-deps": "off",
17 | "@next/next/no-img-element": "off",
18 | },
19 | },
20 | ];
21 |
22 | export default eslintConfig;
23 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/svg.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.svg" {
2 | const content: string;
3 | export default content;
4 | }
5 |
6 | declare module "*.png" {
7 | const content: string;
8 | export default content;
9 | }
10 |
11 | declare module "*.jpg" {
12 | const content: string;
13 | export default content;
14 | }
15 |
16 | declare module "*.jpeg" {
17 | const content: string;
18 | export default content;
19 | }
20 |
21 | declare module "*.gif" {
22 | const content: string;
23 | export default content;
24 | }
25 |
26 | declare module "*.webp" {
27 | const content: string;
28 | export default content;
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/info/InfoRow.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type InfoRowProps = {
4 | label: string;
5 | rightContent: React.ReactNode | string;
6 | };
7 |
8 | export const InfoRow = ({ label, rightContent }: InfoRowProps) => {
9 | return (
10 |
11 |
{label}
12 |
17 | {rightContent}
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/backend/core/tests/utils/urls.py:
--------------------------------------------------------------------------------
1 | """Utils for testing URLs."""
2 |
3 | import importlib
4 |
5 | from django.urls import clear_url_caches
6 |
7 |
8 | def reload_urls():
9 | """
10 | Reload the URLs. Since the url are loaded based on a
11 | settings value, we need to reload the urls to make the
12 | URL settings based condition effective.
13 | """
14 | import core.urls # pylint:disable=import-outside-toplevel # noqa: PLC0415
15 |
16 | import drive.urls # pylint:disable=import-outside-toplevel # noqa: PLC0415
17 |
18 | importlib.reload(core.urls)
19 | importlib.reload(drive.urls)
20 | clear_url_caches()
21 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/users/hooks/useUserQueries.tsx:
--------------------------------------------------------------------------------
1 | import { getDriver } from "@/features/config/Config";
2 | import { UserFilters } from "@/features/drivers/Driver";
3 | import { useQuery } from "@tanstack/react-query";
4 | import { HookUseQueryOptions } from "@/utils/useQueries";
5 | import { User } from "@/features/drivers/types";
6 | export const useUsers = (
7 | filters?: UserFilters,
8 | options?: HookUseQueryOptions
9 | ) => {
10 | const driver = getDriver();
11 |
12 | return useQuery({
13 | ...options,
14 | queryKey: ["users", filters],
15 | queryFn: () => driver.getUsers(filters),
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | }
19 | },
20 | "include": ["next-env.d.ts", "svg.d.ts", "**/*.ts", "**/*.tsx"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/src/mail/bin/html-to-plain-text:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -eo pipefail
3 | # Run html-to-text to convert all html files to text files
4 | DIR_MAILS="../backend/core/templates/mail/"
5 |
6 | if [ ! -d "${DIR_MAILS}" ]; then
7 | mkdir -p "${DIR_MAILS}";
8 | fi
9 |
10 | if [ ! -d "${DIR_MAILS}"html/ ]; then
11 | mkdir -p "${DIR_MAILS}"html/;
12 | exit;
13 | fi
14 |
15 | for file in "${DIR_MAILS}"html/*.html;
16 | do html-to-text -j ./html-to-text.config.json < "$file" > "${file%.html}".txt; done;
17 |
18 | if [ ! -d "${DIR_MAILS}"text/ ]; then
19 | mkdir -p "${DIR_MAILS}"text/;
20 | fi
21 |
22 | mv "${DIR_MAILS}"html/*.txt "${DIR_MAILS}"text/;
23 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/left-panel/LeftPanelMobile.tsx:
--------------------------------------------------------------------------------
1 | import { Gaufre } from "@/features/ui/components/gaufre/Gaufre";
2 | import { UserProfile } from "@/features/ui/components/user/UserProfile";
3 | import { useResponsive } from "@gouvfr-lasuite/ui-kit";
4 |
5 | export const LeftPanelMobile = () => {
6 | const { isTablet } = useResponsive();
7 |
8 | if (!isTablet) {
9 | return null;
10 | }
11 |
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # local env files
34 | .env*.local
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/generic-disclaimer/GenericDisclaimer.scss:
--------------------------------------------------------------------------------
1 | .drive__generic-disclaimer {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | height: calc(100vh - var(--header-height));
6 | padding: 0 0.5rem;
7 |
8 | &__content {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | justify-content: center;
13 | gap: 1rem;
14 |
15 | &__image {
16 | max-width: 100%;
17 | }
18 |
19 | > p {
20 | margin: 0;
21 | color: var(--c--contextuals--content--semantic--neutral--primary);
22 | text-align: center;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0008_alter_item_options_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.3 on 2025-06-23 09:22
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0007_item_hard_deleted_at'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='item',
15 | options={'ordering': ('created_at',), 'verbose_name': 'Item', 'verbose_name_plural': 'Items'},
16 | ),
17 | migrations.RemoveConstraint(
18 | model_name='item',
19 | name='check_filename_set_for_files',
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/analytics/AnalyticsProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useConfig } from "../config/ConfigProvider";
2 | import { PostHogProvider } from "posthog-js/react";
3 |
4 | export const AnalyticsProvider = ({
5 | children,
6 | }: {
7 | children: React.ReactNode;
8 | }) => {
9 | const { config } = useConfig();
10 |
11 | if (!config?.POSTHOG_KEY) {
12 | return children;
13 | }
14 |
15 | return (
16 |
23 | {children}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/generic-disclaimer/GenericDisclaimer.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from "react";
2 |
3 | export const GenericDisclaimer = ({
4 | message,
5 | imageSrc,
6 | children,
7 | }: {
8 | message: string;
9 | imageSrc: string;
10 | } & PropsWithChildren) => {
11 | return (
12 |
13 |
14 |

19 |
{message}
20 | {children}
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/icons/ItemIcon.scss:
--------------------------------------------------------------------------------
1 | .workspace-icon-container {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 24px;
6 | height: 24px;
7 | border-radius: 4px;
8 | background-color: var(--c--contextuals--background--palette--brand--primary);
9 | border: 1px solid rgba(255, 255, 255, 0.2);
10 | }
11 |
12 | .item-icon {
13 | &.small {
14 | width: 18px;
15 | height: 18px;
16 | }
17 |
18 | &.medium {
19 | width: 32px;
20 | height: 32px;
21 | }
22 |
23 | &.large {
24 | width: 48px;
25 | height: 48px;
26 | }
27 |
28 | &.xlarge {
29 | width: 64px;
30 | height: 64px;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/toasts/addItemsMovedToast.tsx:
--------------------------------------------------------------------------------
1 | import { addToast } from "@/features/ui/components/toaster/Toaster";
2 | import { useTranslation } from "react-i18next";
3 | import { ToasterItem } from "@/features/ui/components/toaster/Toaster";
4 |
5 | export const addItemsMovedToast = (count: number) => {
6 | addToast();
7 | };
8 |
9 | const ItemsMovedToast = ({ count }: { count: number }) => {
10 | const { t } = useTranslation();
11 | return (
12 |
13 | arrow_forward
14 | {t("explorer.actions.move.toast", { count })}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/mail/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mail_mjml",
3 | "version": "0.10.1",
4 | "description": "An util to generate html and text django's templates from mjml templates",
5 | "type": "module",
6 | "dependencies": {
7 | "@html-to/text-cli": "0.5.4",
8 | "mjml": "4.17.0"
9 | },
10 | "private": true,
11 | "scripts": {
12 | "build-mjml-to-html": "bash ./bin/mjml-to-html",
13 | "build-html-to-plain-text": "bash ./bin/html-to-plain-text",
14 | "build": "yarn build-mjml-to-html && yarn build-html-to-plain-text"
15 | },
16 | "volta": {
17 | "node": "24.11.1"
18 | },
19 | "repository": "https://github.com/suitenumerique/drive",
20 | "author": "DINUM",
21 | "license": "MIT"
22 | }
23 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0009_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.9 on 2025-06-26 14:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0008_alter_item_options_and_more'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='user',
15 | name='language',
16 | field=models.CharField(blank=True, choices=[('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German')], default=None, help_text='The language in which the user wants to see the interface.', max_length=10, null=True, verbose_name='language'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/backend/core/api/fields.py:
--------------------------------------------------------------------------------
1 | """A JSONField for DRF to handle serialization/deserialization."""
2 |
3 | import json
4 |
5 | from rest_framework import serializers
6 |
7 |
8 | class JSONField(serializers.Field):
9 | """
10 | A custom field for handling JSON data.
11 | """
12 |
13 | def to_representation(self, value):
14 | """
15 | Convert the JSON string to a Python dictionary for serialization.
16 | """
17 | return value
18 |
19 | def to_internal_value(self, data):
20 | """
21 | Convert the Python dictionary to a JSON string for deserialization.
22 | """
23 | if data is None:
24 | return None
25 | return json.dumps(data)
26 |
--------------------------------------------------------------------------------
/src/backend/core/storage/creator_storage_compute_backend.py:
--------------------------------------------------------------------------------
1 | """
2 | Storage compute backend for calculating storage usage metrics by creator.
3 | """
4 |
5 | from django.db.models import Sum
6 |
7 | from core.models import Item
8 | from core.storage.storage_compute_backend import StorageComputeBackend
9 |
10 |
11 | class CreatorStorageComputeBackend(StorageComputeBackend):
12 | """Storage compute backend for calculating storage usage metrics by creator."""
13 |
14 | def compute_storage_used(self, user):
15 | """
16 | Compute the storage used by a user.
17 | """
18 | return Item.objects.filter(creator=user).aggregate(
19 | total_size=Sum("size", default=0)
20 | )["total_size"]
21 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0014_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.7 on 2025-11-12 09:57
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0013_active_unnaccent_postgres_extension'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='user',
15 | name='language',
16 | field=models.CharField(blank=True, choices=[('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'), ('nl-nl', 'Dutch')], default=None, help_text='The language in which the user wants to see the interface.', max_length=10, null=True, verbose_name='language'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/files/icons/mime-video-mini.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0010_alter_item_upload_state.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.4 on 2025-07-22 15:12
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0009_alter_user_language'),
10 | ('malware_detection', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='item',
16 | name='upload_state',
17 | field=models.CharField(blank=True, choices=[('pending', 'Pending'), ('analyzing', 'Analyzing'), ('suspicious', 'Suspicious'), ('file_too_large_to_analyze', 'File too large to analyze'), ('ready', 'Ready')], max_length=25, null=True),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/auth/components/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@openfun/cunningham-react";
2 | import { login } from "../Auth";
3 | import { useTranslation } from "react-i18next";
4 | import { SESSION_STORAGE_REDIRECT_AFTER_LOGIN_URL } from "@/features/api/fetchApi";
5 |
6 | export const LoginButton = () => {
7 | const { t } = useTranslation();
8 | return (
9 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/hooks/useThemeCustomization.tsx:
--------------------------------------------------------------------------------
1 | import { useConfig } from "@/features/config/ConfigProvider";
2 | import { ThemeCustomization } from "@/features/drivers/types";
3 | import { splitLocaleCode } from "@/features/i18n/utils";
4 | import { useTranslation } from "react-i18next";
5 |
6 | export const useThemeCustomization = (key: keyof ThemeCustomization) => {
7 | const { config } = useConfig();
8 | const { i18n } = useTranslation();
9 | const language = splitLocaleCode(i18n.language).language;
10 | const themeCustomization = config?.theme_customization?.[key];
11 | return {
12 | ...themeCustomization?.default,
13 | ...(themeCustomization?.[language as keyof typeof themeCustomization] ??
14 | {}),
15 | };
16 | };
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug Report
3 | about: If something is not working as expected 🤔.
4 |
5 | ---
6 |
7 | ## Bug Report
8 |
9 | **Problematic behavior**
10 | A clear and concise description of the behavior.
11 |
12 | **Expected behavior/code**
13 | A clear and concise description of what you expected to happen (or code).
14 |
15 | **Steps to Reproduce**
16 | 1. Do this...
17 | 2. Then this...
18 | 3. And then the bug happens!
19 |
20 | **Environment**
21 | - Drive version:
22 | - Platform:
23 |
24 | **Possible Solution**
25 |
26 |
27 | **Additional context/Screenshots**
28 | Add any other context about the problem here. If applicable, add screenshots to help explain.
29 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/suspicious/SuspiciousPreview.scss:
--------------------------------------------------------------------------------
1 | .file-preview-suspicious {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | height: 100%;
7 | gap: var(--c--globals--spacings--sm);
8 |
9 | &__title {
10 | color: var(--c--contextuals--content--semantic--neutral--primary);
11 | text-align: center;
12 | font-size: 16px;
13 | font-style: normal;
14 | font-weight: 700;
15 | line-height: 22px;
16 | }
17 |
18 | &__description {
19 | color: var(--c--contextuals--content--semantic--neutral--secondary);
20 | text-align: center;
21 | font-size: 12px;
22 | font-style: normal;
23 | font-weight: 400;
24 | line-height: 16px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/401.tsx:
--------------------------------------------------------------------------------
1 | import { login } from "@/features/auth/Auth";
2 | import { getSimpleLayout } from "@/features/layouts/components/simple/SimpleLayout";
3 | import { GenericDisclaimer } from "@/features/ui/components/generic-disclaimer/GenericDisclaimer";
4 | import { Button } from "@openfun/cunningham-react";
5 | import { useTranslation } from "react-i18next";
6 |
7 | export default function UnauthorizedPage() {
8 | const { t } = useTranslation();
9 | return (
10 |
14 |
15 |
16 | );
17 | }
18 |
19 | UnauthorizedPage.getLayout = getSimpleLayout;
20 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "verbatimModuleSyntax": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "erasableSyntaxOnly": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true
23 | },
24 | "include": ["vite.config.ts"]
25 | }
26 |
--------------------------------------------------------------------------------
/src/backend/wopi/permissions.py:
--------------------------------------------------------------------------------
1 | """Permissions related to WOPI API."""
2 |
3 | from rest_framework.permissions import BasePermission
4 |
5 |
6 | class AccessTokenPermission(BasePermission):
7 | """
8 | Check if the user has access to the item by checking its abilities
9 | """
10 |
11 | def has_permission(self, request, view):
12 | """
13 | Check if the user has permission to access the item.
14 | request.auth should contains AccessUserItem object.
15 | """
16 | if request.auth is None:
17 | return False
18 |
19 | item = request.auth.item
20 |
21 | if item.id != view.get_file_id():
22 | return False
23 |
24 | abilities = item.get_abilities(request.user)
25 |
26 | return abilities["retrieve"]
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/sdk/SdkRelayManager.ts:
--------------------------------------------------------------------------------
1 | import { fetchAPI } from "../api/fetchApi";
2 |
3 | export enum ClientMessageType {
4 | // Picker.
5 | ITEMS_SELECTED = "ITEMS_SELECTED",
6 | CANCEL = "CANCEL",
7 | // Saver
8 | SAVER_READY = "SAVER_READY",
9 | SAVER_PAYLOAD = "SAVER_PAYLOAD",
10 | ITEM_SAVED = "ITEM_SAVED",
11 | }
12 |
13 | export interface SDKRelayEvent {
14 | type: string;
15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
16 | data: any;
17 | }
18 |
19 | export class SDKRelayManager {
20 | static async registerEvent(token: string, event: SDKRelayEvent) {
21 | return await fetchAPI(`sdk-relay/events/`, {
22 | method: "POST",
23 | body: JSON.stringify({
24 | token,
25 | event,
26 | }),
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/403.tsx:
--------------------------------------------------------------------------------
1 | import { getSimpleLayout } from "@/features/layouts/components/simple/SimpleLayout";
2 | import { GenericDisclaimer } from "@/features/ui/components/generic-disclaimer/GenericDisclaimer";
3 | import { Icon } from "@gouvfr-lasuite/ui-kit";
4 | import { Button } from "@openfun/cunningham-react";
5 | import { useTranslation } from "react-i18next";
6 |
7 | export default function UnauthorizedPage() {
8 | const { t } = useTranslation();
9 | return (
10 |
14 | }>
15 | {t("403.button")}
16 |
17 |
18 | );
19 | }
20 |
21 | UnauthorizedPage.getLayout = getSimpleLayout;
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sdk-consumer",
3 | "private": true,
4 | "version": "0.1.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "19.2.0",
14 | "react-dom": "19.2.0"
15 | },
16 | "devDependencies": {
17 | "@eslint/js": "^9.25.0",
18 | "@types/react": "^19.1.2",
19 | "@types/react-dom": "^19.1.2",
20 | "@vitejs/plugin-react": "^5.0.0",
21 | "eslint": "^9.25.0",
22 | "eslint-plugin-react-hooks": "^7.0.0",
23 | "eslint-plugin-react-refresh": "^0.4.19",
24 | "globals": "^16.0.0",
25 | "typescript": "~5.9.0",
26 | "typescript-eslint": "^8.30.1",
27 | "vite": "^6.3.5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/user/UserProfile.tsx:
--------------------------------------------------------------------------------
1 | import { UserMenu } from "@gouvfr-lasuite/ui-kit";
2 | import { useAuth } from "@/features/auth/Auth";
3 | import { logout } from "@/features/auth/Auth";
4 | import { LanguagePickerUserMenu } from "@/features/layouts/components/header/Header";
5 | import { LoginButton } from "@/features/auth/components/LoginButton";
6 |
7 | export const UserProfile = () => {
8 | const { user } = useAuth();
9 | return (
10 | <>
11 | {user ? (
12 | }
17 | />
18 | ) : (
19 |
20 | )}
21 | >
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/backend_svc.yaml:
--------------------------------------------------------------------------------
1 | {{- $envVars := include "drive.common.env" (list . .Values.backend) -}}
2 | {{- $fullName := include "drive.backend.fullname" . -}}
3 | {{- $component := "backend" -}}
4 | apiVersion: v1
5 | kind: Service
6 | metadata:
7 | name: {{ $fullName }}
8 | namespace: {{ .Release.Namespace | quote }}
9 | labels:
10 | {{- include "drive.common.labels" (list . $component) | nindent 4 }}
11 | annotations:
12 | {{- toYaml $.Values.backend.service.annotations | nindent 4 }}
13 | spec:
14 | type: {{ .Values.backend.service.type }}
15 | ports:
16 | - port: {{ .Values.backend.service.port }}
17 | targetPort: {{ .Values.backend.service.targetPort }}
18 | protocol: TCP
19 | name: http
20 | selector:
21 | {{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
22 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/frontend_svc.yaml:
--------------------------------------------------------------------------------
1 | {{- $envVars := include "drive.common.env" (list . .Values.frontend) -}}
2 | {{- $fullName := include "drive.frontend.fullname" . -}}
3 | {{- $component := "frontend" -}}
4 | apiVersion: v1
5 | kind: Service
6 | metadata:
7 | name: {{ $fullName }}
8 | namespace: {{ .Release.Namespace | quote }}
9 | labels:
10 | {{- include "drive.common.labels" (list . $component) | nindent 4 }}
11 | annotations:
12 | {{- toYaml $.Values.frontend.service.annotations | nindent 4 }}
13 | spec:
14 | type: {{ .Values.frontend.service.type }}
15 | ports:
16 | - port: {{ .Values.frontend.service.port }}
17 | targetPort: {{ .Values.frontend.service.targetPort }}
18 | protocol: TCP
19 | name: http
20 | selector:
21 | {{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/error/ErrorPreview.scss:
--------------------------------------------------------------------------------
1 | .file-preview-error {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | gap: var(--c--globals--spacings--sm);
7 | height: 100%;
8 |
9 | &__title {
10 | color: var(--c--contextuals--content--semantic--error--primary);
11 | text-align: center;
12 | font-size: 16px;
13 | font-style: normal;
14 | font-weight: 700;
15 | line-height: 22px;
16 | }
17 |
18 | &__description {
19 | color: var(--c--contextuals--content--semantic--neutral--secondary);
20 | text-align: center;
21 | font-size: 12px;
22 | font-style: normal;
23 | font-weight: 400;
24 | line-height: 16px;
25 | }
26 |
27 | &__download-button {
28 | margin-top: var(--c--globals--spacings--xs);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/not-supported/NotSupportedPreview.scss:
--------------------------------------------------------------------------------
1 | .file-preview-unsupported {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | gap: var(--c--globals--spacings--xs);
7 |
8 | &__title {
9 | color: var(--c--contextuals--content--semantic--neutral--primary);
10 | text-align: center;
11 | font-size: 16px;
12 | font-style: normal;
13 | font-weight: 700;
14 | line-height: 22px;
15 | }
16 |
17 | &__description {
18 | color: var(--c--contextuals--content--semantic--neutral--secondary);
19 | text-align: center;
20 | font-size: 12px;
21 | font-style: normal;
22 | font-weight: 400;
23 | line-height: 16px;
24 | }
25 |
26 | &__download-button {
27 | margin-top: var(--c--globals--spacings--md);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "verbatimModuleSyntax": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "erasableSyntaxOnly": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noUncheckedSideEffectImports": true
25 | },
26 | "include": ["src"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/Draggable.tsx:
--------------------------------------------------------------------------------
1 | import { Item } from "@/features/drivers/types";
2 | import { useDraggable } from "@dnd-kit/core";
3 |
4 | type DraggableProps = {
5 | disabled?: boolean;
6 | item: Item;
7 | children: React.ReactNode;
8 | id: string;
9 | style?: React.CSSProperties;
10 | className?: string;
11 | };
12 | export const Draggable = (props: DraggableProps) => {
13 | const { attributes, listeners, setNodeRef } = useDraggable({
14 | id: props.id,
15 | disabled: props.disabled,
16 | data: {
17 | item: props.item,
18 | },
19 | });
20 |
21 | return (
22 |
29 | {props.children}
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/backend/wopi/tests/services/test_lock.py:
--------------------------------------------------------------------------------
1 | """Test the LockService."""
2 |
3 | import pytest
4 |
5 | from core import factories
6 | from wopi.services.lock import LockService
7 |
8 | pytestmark = pytest.mark.django_db
9 |
10 |
11 | def test_lock_service():
12 | """Test the lock service."""
13 | item = factories.ItemFactory()
14 |
15 | lock_service = LockService(item)
16 | assert lock_service.is_locked() is False
17 |
18 | lock_service.lock("1234567890")
19 | assert lock_service.is_locked()
20 | assert lock_service.get_lock() == "1234567890"
21 | assert lock_service.is_lock_valid("1234567890") is True
22 | assert lock_service.is_lock_valid("1234567891") is False
23 |
24 | lock_service.refresh_lock()
25 |
26 | lock_service.unlock()
27 | assert lock_service.is_locked() is False
28 | assert lock_service.get_lock() is None
29 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["github>numerique-gouv/renovate-configuration"],
3 | "dependencyDashboard": true,
4 | "labels": ["dependencies", "noChangeLog"],
5 | "packageRules": [
6 | {
7 | "groupName": "allowed redis versions",
8 | "matchManagers": ["pep621"],
9 | "matchPackageNames": ["redis"],
10 | "allowedVersions": "<6.0.0"
11 | },
12 | {
13 | "groupName": "allowed pylint versions",
14 | "matchManagers": ["pep621"],
15 | "matchPackageNames": ["pylint"],
16 | "allowedVersions": "<4.0.0"
17 | },
18 | {
19 | "description": "Disable requires-python updates - managed manually",
20 | "matchManagers": ["pep621"],
21 | "matchPackageNames": ["python"],
22 | "enabled": false
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/tree/folder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/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 tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0011_update_items_upload_state_value.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.4 on 2025-07-22 15:12
2 |
3 | from django.db import migrations, models
4 |
5 | def update_items_upload_state_value(apps, schema_editor):
6 | Item = apps.get_model('core', 'Item')
7 | Item.objects.filter(upload_state='uploaded').update(upload_state='ready')
8 |
9 | def reverse_update_items_upload_state_value(apps, schema_editor):
10 | Item = apps.get_model('core', 'Item')
11 | Item.objects.filter(upload_state='ready').update(upload_state='uploaded')
12 |
13 |
14 | class Migration(migrations.Migration):
15 |
16 | dependencies = [
17 | ('core', '0010_alter_item_upload_state'),
18 | ]
19 |
20 | operations = [
21 | migrations.RunPython(
22 | update_items_upload_state_value, reverse_code=reverse_update_items_upload_state_value
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/bin/update_app_cacert.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -o errexit
3 |
4 | # The script is pretty simple. It downloads the latest cacert.pem file from the certifi package and appends the root certificate from mkcert to it. Then it copies the updated cacert.pem file to the container.
5 | # The script is executed with the following command:
6 | # $ bin/update_app_cacert.sh docs-production-backend-1
7 |
8 | CONTAINER_NAME=${1:-"drive-production-backend-1"}
9 |
10 | echo "updating cacert.pem for certifi package in ${CONTAINER_NAME}"
11 |
12 |
13 | curl --create-dirs https://raw.githubusercontent.com/certifi/python-certifi/refs/heads/master/certifi/cacert.pem -o /tmp/certifi/cacert.pem
14 | cat "$(mkcert -CAROOT)/rootCA.pem" >> /tmp/certifi/cacert.pem
15 | docker cp /tmp/certifi/cacert.pem ${CONTAINER_NAME}:/usr/local/lib/python3.12/site-packages/certifi/cacert.pem
16 |
17 | echo "end patching cacert.pem in ${CONTAINER_NAME}"
18 |
--------------------------------------------------------------------------------
/docs/theming.md:
--------------------------------------------------------------------------------
1 | # Theming 📝
2 |
3 | ## **Footer Theming**
4 |
5 | The footer is configurable from the theme customization file.
6 |
7 | ### Settings 🔧
8 |
9 | ```shellscript
10 | THEME_CUSTOMIZATION_FILE_PATH=
11 | ```
12 |
13 | ### Example of a JSON customization file
14 |
15 | The json must follow some rules: https://github.com/suitenumerique/drive/blob/main/src/backend/drive/configuration/theme/default.json
16 |
17 | `footer.default` is the fallback if the language is not supported.
18 |
19 | ### Custom translations in the footer 📝
20 |
21 | The translations can be partially overridden from the theme customization file.
22 |
23 | ### Settings 🔧
24 |
25 | ```shellscript
26 | THEME_CUSTOMIZATION_FILE_PATH=
27 | ```
28 |
29 | ### Example of a JSON customization file
30 |
31 | The json must follow some rules: https://github.com/suitenumerique/drive/blob/main/src/backend/drive/configuration/theme/default.json
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/files/icons/mime-other-mini.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/.github/workflows/helmfile-linter.yaml:
--------------------------------------------------------------------------------
1 | name: Helmfile lint
2 | run-name: Helmfile lint
3 |
4 | on:
5 | push:
6 | pull_request:
7 | branches:
8 | - 'main'
9 |
10 | jobs:
11 | helmfile-lint:
12 | runs-on: ubuntu-latest
13 | container:
14 | image: ghcr.io/helmfile/helmfile:v0.171.0
15 | steps:
16 | -
17 | name: Checkout repository
18 | uses: actions/checkout@v6
19 | -
20 | name: Helmfile lint
21 | shell: bash
22 | run: |
23 | set -e
24 | HELMFILE=src/helm/helmfile.yaml
25 | environments=$(awk 'BEGIN {in_env=0} /^environments:/ {in_env=1; next} /^---/ {in_env=0} in_env && /^ [^ ]/ {gsub(/^ /,""); gsub(/:.*$/,""); print}' "$HELMFILE")
26 | for env in $environments; do
27 | echo "################### $env lint ###################"
28 | helmfile -e $env -f $HELMFILE lint || exit 1
29 | echo -e "\n"
30 | done
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/header/index.scss:
--------------------------------------------------------------------------------
1 | @use "sass:map";
2 | @use "@/styles/cunningham-tokens-sass" as *;
3 |
4 | $tablet: map.get($themes, "default", "globals", "breakpoints", "tablet");
5 |
6 | @media (max-width: $tablet) {
7 | .c__header {
8 | border-bottom: 0;
9 | }
10 | }
11 |
12 | .c__header__right {
13 | display: flex;
14 | align-items: center;
15 | gap: 0.5rem;
16 |
17 | .explorer__search__button {
18 | display: none;
19 | }
20 |
21 | @media (max-width: $tablet) {
22 | .drive__header__login-button {
23 | display: none;
24 | }
25 |
26 | .c__feedback__button {
27 | display: none;
28 | }
29 |
30 | .c__dropdown-menu-trigger {
31 | display: none;
32 | }
33 |
34 | .explorer__search__button {
35 | display: flex;
36 | }
37 |
38 | .lasuite-gaufre-btn {
39 | display: none !important;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/tree/nav/ExplorerTreeNav.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from "react-i18next";
2 | import trashIcon from "@/assets/icons/trash.svg";
3 | import { ExplorerTreeNavItem } from "./ExplorerTreeNavItem";
4 | import { HorizontalSeparator } from "@gouvfr-lasuite/ui-kit";
5 |
6 | export const ExplorerTreeNav = () => {
7 | const { t } = useTranslation();
8 |
9 | const navItems = [
10 | {
11 | icon:
,
12 | label: t("explorer.tree.trash"),
13 | route: "/explorer/trash",
14 | },
15 | ];
16 |
17 | return (
18 |
19 |
20 |
21 | {navItems.map((item) => (
22 |
23 | ))}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/hooks/useInfiniteItems.ts:
--------------------------------------------------------------------------------
1 | import { useInfiniteQuery } from "@tanstack/react-query";
2 | import { getDriver } from "@/features/config/Config";
3 | import { ItemFilters } from "@/features/drivers/Driver";
4 |
5 | export const useInfiniteItems = (
6 | filters: ItemFilters = {},
7 | enabled: boolean = true
8 | ) => {
9 | return useInfiniteQuery({
10 | queryKey: [
11 | "items",
12 | "infinite",
13 | ...(Object.keys(filters).length ? [JSON.stringify(filters)] : []),
14 | ],
15 | queryFn: ({ pageParam = 1 }) => {
16 | return getDriver().getItems({
17 | page: pageParam,
18 | ...filters,
19 | });
20 | },
21 | getNextPageParam: (lastPage) => {
22 | return lastPage.pagination.hasMore
23 | ? lastPage.pagination.currentPage + 1
24 | : undefined;
25 | },
26 | initialPageParam: 1,
27 | enabled: enabled,
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/gaufre/Gaufre.tsx:
--------------------------------------------------------------------------------
1 | import { LaGaufreV2 } from "@gouvfr-lasuite/ui-kit";
2 | import {
3 | removeQuotes,
4 | useCunninghamTheme,
5 | } from "../../cunningham/useCunninghamTheme";
6 | import { useConfig } from "@/features/config/ConfigProvider";
7 | import { useAppContext } from "@/pages/_app";
8 |
9 | export const Gaufre = () => {
10 | const { config } = useConfig();
11 | const { theme: themeName } = useAppContext();
12 | const hideGaufre = config?.FRONTEND_HIDE_GAUFRE;
13 | const theme = useCunninghamTheme();
14 | const widgetPath = removeQuotes(theme.components.gaufre.widgetPath);
15 | const apiUrl = removeQuotes(theme.components.gaufre.apiUrl);
16 |
17 | if (hideGaufre) {
18 | return null;
19 | }
20 |
21 | return (
22 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/backend/core/migrations/0015_user_claims_alter_user_language.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.8 on 2025-11-13 10:04
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('core', '0014_alter_user_language'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='user',
15 | name='claims',
16 | field=models.JSONField(blank=True, default=dict, help_text='Claims from the OIDC token.'),
17 | ),
18 | migrations.AlterField(
19 | model_name='user',
20 | name='language',
21 | field=models.CharField(blank=True, choices=[('en-us', 'English'), ('fr-fr', 'French'), ('de-de', 'German'), ('nl-nl', 'Dutch')], default=None, help_text='The language in which the user wants to see the interface.', max_length=10, null=True, verbose_name='language'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path, { resolve } from "path";
2 |
3 | import react from "@vitejs/plugin-react-swc";
4 | import { AliasOptions, defineConfig } from "vite";
5 | import dts from "vite-plugin-dts";
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [
10 | react(),
11 | dts({ tsconfigPath: "./tsconfig.app.json", rollupTypes: true }),
12 | ],
13 | resolve: {
14 | alias: {
15 | "@": path.resolve(__dirname, "src"),
16 | } as AliasOptions,
17 | },
18 | build: {
19 | lib: {
20 | entry: resolve(__dirname, "src/index.ts"),
21 | name: "drive-sdk",
22 | fileName: "drive-sdk",
23 | },
24 | rollupOptions: {
25 | external: ["react", "react-dom"],
26 | output: {
27 | globals: {
28 | react: "React",
29 | "react-dom": "ReactDOM",
30 | },
31 | },
32 | },
33 | copyPublicDir: false,
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Support_question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤗 Support Question
3 | about: If you have a question 💬, or something was not clear from the docs!
4 |
5 | ---
6 |
7 |
9 |
10 | ---
11 |
12 | Please make sure you have read our [main Readme](https://github.com/suitenumerique/drive).
13 |
14 | Also make sure it was not already answered in [an open or close issue](https://github.com/suitenumerique/drive/issues).
15 |
16 | If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
17 |
18 | **Topic**
19 | What's the general area of your question: for example, docker setup, database schema, search functionality,...
20 |
21 | **Question**
22 | Try to be as specific as possible so we can help you as best we can. Please be patient 🙏
23 |
--------------------------------------------------------------------------------
/src/backend/core/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Unit tests for the User model
3 | """
4 |
5 | import pytest
6 |
7 | from drive.settings import Base
8 |
9 |
10 | def test_invalid_settings_oidc_email_configuration():
11 | """
12 | The OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and OIDC_ALLOW_DUPLICATE_EMAILS settings
13 | should not be both set to True simultaneously.
14 | """
15 |
16 | class TestSettings(Base):
17 | """Fake test settings."""
18 |
19 | OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
20 | OIDC_ALLOW_DUPLICATE_EMAILS = True
21 |
22 | # The validation is performed during post_setup
23 | with pytest.raises(ValueError) as excinfo:
24 | TestSettings().post_setup()
25 |
26 | # Check the exception message
27 | assert str(excinfo.value) == (
28 | "Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
29 | "OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
30 | )
31 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/posthog_svc.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.posthog.ingress.enabled -}}
2 | {{- $envVars := include "drive.common.env" (list . .Values.posthog) -}}
3 | {{- $fullName := include "drive.posthog.fullname" . -}}
4 | {{- $component := "posthog" -}}
5 | apiVersion: v1
6 | kind: Service
7 | metadata:
8 | name: {{ $fullName }}-proxy
9 | namespace: {{ .Release.Namespace | quote }}
10 | labels:
11 | {{- include "drive.common.labels" (list . $component) | nindent 4 }}
12 | annotations:
13 | {{- toYaml $.Values.posthog.service.annotations | nindent 4 }}
14 | spec:
15 | type: {{ .Values.posthog.service.type }}
16 | externalName: {{ .Values.posthog.service.externalName }}
17 | ports:
18 | - port: {{ .Values.posthog.service.port }}
19 | targetPort: {{ .Values.posthog.service.targetPort }}
20 | protocol: TCP
21 | name: https
22 | selector:
23 | {{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
24 | {{- end }}
25 |
--------------------------------------------------------------------------------
/src/backend/core/authentication/views.py:
--------------------------------------------------------------------------------
1 | """Drive core authentication views."""
2 |
3 | from django.http import HttpResponseRedirect
4 |
5 | from lasuite.oidc_login.views import (
6 | OIDCAuthenticationCallbackView as LaSuiteOIDCAuthenticationCallbackView,
7 | )
8 |
9 | from core.authentication.exceptions import UserCannotAccessApp
10 |
11 |
12 | class OIDCAuthenticationCallbackView(LaSuiteOIDCAuthenticationCallbackView):
13 | """
14 | Custom view for handling the authentication callback from the OpenID Connect (OIDC) provider.
15 | Handles the callback after authentication from the identity provider (OP).
16 | Verifies the state parameter and performs necessary authentication actions.
17 | """
18 |
19 | def get(self, request):
20 | try:
21 | return super().get(request)
22 | except UserCannotAccessApp:
23 | return HttpResponseRedirect(
24 | self.failure_url + "?auth_error=user_cannot_access_app"
25 | )
26 |
--------------------------------------------------------------------------------
/src/backend/drive/celery_app.py:
--------------------------------------------------------------------------------
1 | """Drive celery configuration file."""
2 |
3 | import os
4 |
5 | from celery import Celery
6 | from configurations.importer import install
7 |
8 | # Set the default Django settings module for the 'celery' program.
9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "drive.settings")
10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
11 |
12 | install(check_options=True)
13 |
14 | # Can be loaded only after install call.
15 | from django.conf import settings # pylint: disable=wrong-import-position
16 |
17 | app = Celery("drive")
18 |
19 | # Using a string here means the worker doesn't have to serialize
20 | # the configuration object to child processes.
21 | # - namespace='CELERY' means all celery-related configuration keys
22 | # should have a `CELERY_` prefix.
23 | app.config_from_object("django.conf:settings", namespace="CELERY")
24 |
25 | # Load task modules from all registered Django apps.
26 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/drivers/implementations/DummyDriver.ts:
--------------------------------------------------------------------------------
1 | // import { Driver } from "../Driver";
2 | // import { Item, ItemType } from "../types";
3 |
4 | // export class DummyDriver extends Driver {
5 | // async getItems(): Promise- {
6 | // return [
7 | // {
8 | // id: "1",
9 | // title: "Mon Espace",
10 | // type: ItemType.FOLDER,
11 | // lastUpdate: new Date().toISOString(),
12 | // },
13 | // ];
14 | // }
15 |
16 | // async getItem(id: string): Promise
- {
17 | // return {
18 | // id: "1",
19 | // title: "Mon Espace",
20 | // type: ItemType.FOLDER,
21 | // lastUpdate: new Date().toISOString(),
22 | // };
23 | // }
24 |
25 | // async createFolder(data: { title: string }): Promise
- {
26 | // return {
27 | // id: "1",
28 | // title: data.title,
29 | // type: ItemType.FOLDER,
30 | // lastUpdate: new Date().toISOString(),
31 | // };
32 | // }
33 | // }
34 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/item/right-content-info.spec.ts:
--------------------------------------------------------------------------------
1 | import test, { expect } from "@playwright/test";
2 | import { clearDb, login } from "../utils-common";
3 | import { clickOnRowItemActions, getRowItem } from "../utils-embedded-grid";
4 |
5 | test("Check that the right content is displayed correctly", async ({
6 | page,
7 | }) => {
8 | await clearDb();
9 | await login(page, "drive@example.com");
10 | await page.goto("/");
11 | await page.getByRole("button", { name: "add Create" }).click();
12 | await page.getByRole("menuitem", { name: "New folder" }).click();
13 | await page.getByRole("textbox", { name: "Folder name" }).fill("testFolder");
14 | await page.getByRole("button", { name: "Create" }).click();
15 | await getRowItem(page, "testFolder");
16 | await clickOnRowItemActions(page, "testFolder", "Info");
17 | const rightPanel = page.getByTestId("right-panel");
18 | await expect(rightPanel).toBeVisible();
19 | await expect(rightPanel.getByText("testFolder")).toBeVisible();
20 | });
21 |
--------------------------------------------------------------------------------
/src/helm/drive/templates/posthog_assets_svc.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.posthog.ingressAssets.enabled -}}
2 | {{- $envVars := include "drive.common.env" (list . .Values.posthog) -}}
3 | {{- $fullName := include "drive.posthog.fullname" . -}}
4 | {{- $component := "posthog" -}}
5 | apiVersion: v1
6 | kind: Service
7 | metadata:
8 | name: {{ $fullName }}-assets-proxy
9 | namespace: {{ .Release.Namespace | quote }}
10 | labels:
11 | {{- include "drive.common.labels" (list . $component) | nindent 4 }}
12 | annotations:
13 | {{- toYaml $.Values.posthog.assetsService.annotations | nindent 4 }}
14 | spec:
15 | type: {{ .Values.posthog.assetsService.type }}
16 | externalName: {{ .Values.posthog.assetsService.externalName }}
17 | ports:
18 | - port: {{ .Values.posthog.assetsService.port }}
19 | targetPort: {{ .Values.posthog.assetsService.targetPort }}
20 | protocol: TCP
21 | name: https
22 | selector:
23 | {{- include "drive.common.selectorLabels" (list . $component) | nindent 4 }}
24 | {{- end }}
25 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ES2020",
6 | "useDefineForClassFields": true,
7 | "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"],
8 | "module": "esnext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": false,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": true,
23 | "noUnusedParameters": true,
24 | "noFallthroughCasesInSwitch": true,
25 |
26 | /* Local */
27 | "allowSyntheticDefaultImports": true,
28 | "esModuleInterop": true,
29 |
30 | "baseUrl": "./src",
31 | "paths": {
32 | "@/*": ["./*"]
33 | }
34 | },
35 | "include": ["src"]
36 | }
37 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/logo-icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/folder/folder.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/backend/demo/tests/test_commands_create_demo.py:
--------------------------------------------------------------------------------
1 | """Test the `create_demo` management command"""
2 |
3 | from unittest import mock
4 |
5 | from django.core.management import call_command
6 | from django.test import override_settings
7 |
8 | import pytest
9 |
10 | from core import models
11 |
12 | pytestmark = pytest.mark.django_db
13 |
14 |
15 | @mock.patch(
16 | "demo.defaults.NB_OBJECTS",
17 | {
18 | "users": 10,
19 | "files": 10,
20 | "max_users_per_document": 5,
21 | },
22 | )
23 | @override_settings(DEBUG=True)
24 | def test_commands_create_demo():
25 | """The create_demo management command should create objects as expected."""
26 | call_command("create_demo")
27 |
28 | assert models.User.objects.count() >= 10
29 | assert models.Item.objects.count() >= 10
30 | assert models.ItemAccess.objects.count() > 10
31 |
32 | # assert dev users have doc accesses
33 | user = models.User.objects.get(email="drive@drive.world")
34 | assert models.ItemAccess.objects.filter(user=user).exists()
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ✨ Feature Request
3 | about: I have a suggestion (and may want to build it 💪)!
4 |
5 | ---
6 |
7 | ## Feature Request
8 |
9 | **Is your feature request related to a problem or unsupported use case? Please describe.**
10 | A clear and concise description of what the problem is. For example: I need to do some task and I have an issue...
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen. Add any considered drawbacks.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Discovery, Documentation, Adoption, Migration Strategy**
19 | If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
20 | Maybe a screenshot or design?
21 |
22 | **Do you want to work on it through a Pull Request?**
23 |
24 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/breadcrumbs/index.scss:
--------------------------------------------------------------------------------
1 | .c__breadcrumbs {
2 | display: flex;
3 | align-items: center;
4 | overflow: auto;
5 |
6 | &__separator {
7 | color: var(--c--contextuals--content--semantic--neutral--tertiary);
8 | }
9 |
10 | > * {
11 | flex-shrink: 0;
12 | }
13 | }
14 |
15 | .c__breadcrumbs__button {
16 | height: 32px;
17 | padding: 4px;
18 | background-color: transparent;
19 | border: none;
20 | color: var(--c--contextuals--content--semantic--neutral--secondary);
21 | font-size: 16px;
22 | border-radius: 4px;
23 | font-weight: 400;
24 | font-family: var(--c--globals--font--families--base);
25 | cursor: pointer;
26 | display: flex;
27 | align-items: center;
28 | gap: 8px;
29 | text-decoration: none;
30 |
31 | &:hover {
32 | background-color: var(
33 | --c--contextuals--background--semantic--neutral--tertiary
34 | );
35 | }
36 |
37 | &.active {
38 | font-weight: 600;
39 | color: var(--c--contextuals--content--semantic--neutral--primary);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/api/utils.ts:
--------------------------------------------------------------------------------
1 | export const errorCauses = async (response: Response, data?: unknown) => {
2 | const errorsBody = (await response.json()) as Record<
3 | string,
4 | string | string[]
5 | > | null;
6 |
7 | const causes = errorsBody
8 | ? Object.entries(errorsBody)
9 | .map(([, value]) => value)
10 | .flat()
11 | : undefined;
12 |
13 | return {
14 | status: response.status,
15 | cause: causes,
16 | data,
17 | };
18 | };
19 |
20 | export const getOrigin = () => {
21 | return (
22 | process.env.NEXT_PUBLIC_API_ORIGIN ||
23 | (typeof window !== "undefined" ? window.location.origin : "")
24 | );
25 | };
26 | export const baseApiUrl = (apiVersion: string = "1.0") => {
27 | const origin = getOrigin();
28 | return `${origin}/api/v${apiVersion}/`;
29 | };
30 |
31 | export const isJson = (str: string) => {
32 | try {
33 | JSON.parse(str);
34 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
35 | } catch (e) {
36 | return false;
37 | }
38 | return true;
39 | };
40 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/create_folder.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/create-folder.spec.ts:
--------------------------------------------------------------------------------
1 | import test, { expect } from "@playwright/test";
2 | import { clearDb, getStorageState, login } from "./utils-common";
3 |
4 | test("Create a folder", async ({ page }) => {
5 | await clearDb();
6 | await login(page, "drive@example.com");
7 |
8 | await page.goto("/");
9 |
10 | await expect(page.getByText("Drop your files here")).toBeVisible();
11 | await expect(
12 | page.getByRole("cell", { name: "My first folder" })
13 | ).not.toBeVisible();
14 | await page.getByRole("button", { name: "add Create" }).click();
15 | await page.getByText("New folder").click();
16 | await page
17 | .getByRole("textbox", { name: "Folder name" })
18 | .fill("My first folder");
19 | await page.getByRole("button", { name: "Create" }).click();
20 |
21 | await expect(page.getByText("Drop your files here")).not.toBeVisible();
22 | await expect(
23 | page.getByRole("cell", { name: "My first folder", exact: true })
24 | ).toBeVisible();
25 | await page.getByRole("cell", { name: "few seconds ago" }).click();
26 | });
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/hooks/useCopyToClipboard.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | addToast,
3 | ToasterItem,
4 | } from "@/features/ui/components/toaster/Toaster";
5 | import { useCallback } from "react";
6 | import { useTranslation } from "react-i18next";
7 |
8 | export const useClipboard = () => {
9 | const { t } = useTranslation();
10 |
11 | return useCallback(
12 | (text: string, successMessage?: string, errorMessage?: string) => {
13 | navigator.clipboard
14 | .writeText(text)
15 | .then(() => {
16 | addToast(
17 |
18 | check
19 | {successMessage ?? t("clipboard.success")}
20 |
21 | );
22 | })
23 | .catch(() => {
24 | addToast(
25 |
26 | error
27 | {errorMessage ?? t("clipboard.error")}
28 |
29 | );
30 | });
31 | },
32 | [t]
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/.github/workflows/release-helm-chart.yaml:
--------------------------------------------------------------------------------
1 | name: Release Chart
2 | run-name: Release Chart
3 |
4 | on:
5 | push:
6 | paths:
7 | - src/helm/drive/**
8 |
9 | jobs:
10 | release:
11 | # depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
12 | # see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
13 | permissions:
14 | contents: write
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v6
19 | with:
20 | fetch-depth: 0
21 |
22 | - name: Cleanup
23 | run: rm -rf ./src/helm/extra
24 |
25 | - name: Install Helm
26 | uses: azure/setup-helm@v4
27 | env:
28 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
29 |
30 | - name: Publish Helm charts
31 | uses: numerique-gouv/helm-gh-pages@add-overwrite-option
32 | with:
33 | charts_dir: ./src/helm
34 | token: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/src/backend/core/management/commands/update_suspicious_item_file_hash.py:
--------------------------------------------------------------------------------
1 | """Update suspicious item file hash"""
2 |
3 | from django.core.management.base import BaseCommand
4 |
5 | from core.models import Item, ItemUploadStateChoices
6 | from core.tasks.item import update_suspicious_item_file_hash
7 |
8 |
9 | class Command(BaseCommand):
10 | """Update suspicious item file hash command"""
11 |
12 | help = "Update suspicious item file hash"
13 |
14 | def handle(self, *args, **options):
15 | """Update suspicious item file hash"""
16 | self.stdout.write("Starting update suspicious item file hash command")
17 | items = Item.objects.filter(
18 | upload_state=ItemUploadStateChoices.SUSPICIOUS,
19 | malware_detection_info__file_hash__isnull=True,
20 | ).iterator()
21 |
22 | for item in items:
23 | self.stdout.write(
24 | f"Triggering update suspicious item file hash for item {item.id}"
25 | )
26 | update_suspicious_item_file_hash.delay(item.id)
27 | self.stdout.write("Update suspicious item file hash command completed")
28 |
--------------------------------------------------------------------------------
/bin/pylint:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # shellcheck source=bin/_config.sh
4 | source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
5 |
6 | declare diff_from
7 | declare -a paths
8 | declare -a args
9 |
10 | # Parse options
11 | for arg in "$@"
12 | do
13 | case $arg in
14 | --diff-only=*)
15 | diff_from="${arg#*=}"
16 | shift
17 | ;;
18 | -*)
19 | args+=("$arg")
20 | shift
21 | ;;
22 | *)
23 | paths+=("$arg")
24 | shift
25 | ;;
26 | esac
27 | done
28 |
29 | if [[ -n "${diff_from}" ]]; then
30 | # Run pylint only on modified files located in src/backend
31 | # (excluding deleted files and migration files)
32 | # shellcheck disable=SC2207
33 | paths=($(git diff "${diff_from}" --name-only --diff-filter=d -- src/backend ':!**/migrations/*.py' | grep -E '^src/backend/.*\.py$'))
34 | fi
35 |
36 | # Fix docker vs local path when project sources are mounted as a volume
37 | read -ra paths <<< "$(echo "${paths[@]}" | sed "s|src/backend/||g")"
38 | _dc_run --no-deps app-dev pylint "${paths[@]}" "${args[@]}"
39 |
--------------------------------------------------------------------------------
/src/backend/core/services/sdk_relay.py:
--------------------------------------------------------------------------------
1 | """
2 | This service is used to relay events from the SDK to the backend.
3 | """
4 |
5 | from django.conf import settings
6 | from django.core.cache import cache
7 |
8 |
9 | class SDKRelayManager:
10 | """
11 | This service is used to relay events from the SDK to the backend.
12 | """
13 |
14 | def _get_cache_key(self, token):
15 | """
16 | Get the cache key for the given token.
17 | """
18 | return f"sdk_relay:{token}"
19 |
20 | def register_event(self, token, event):
21 | """
22 | Register an event for the given token.
23 | """
24 | cache.set(
25 | self._get_cache_key(token),
26 | event,
27 | timeout=settings.SDK_RELAY_CACHE_TIMEOUT,
28 | )
29 |
30 | def get_event(self, token):
31 | """
32 | Get the event for the given token.
33 | """
34 | cache_key = self._get_cache_key(token)
35 | data = cache.get(cache_key)
36 |
37 | if not data:
38 | return {}
39 |
40 | cache.delete(cache_key)
41 |
42 | return data
43 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/hooks/useInfiniteChildren.ts:
--------------------------------------------------------------------------------
1 | import { useInfiniteQuery } from "@tanstack/react-query";
2 | import { getDriver } from "@/features/config/Config";
3 | import { ItemFilters } from "@/features/drivers/Driver";
4 |
5 | export const useInfiniteChildren = (
6 | itemId: string | null,
7 | filters: ItemFilters = {},
8 | enabled: boolean = true
9 | ) => {
10 | return useInfiniteQuery({
11 | queryKey: [
12 | "items",
13 | itemId,
14 | "children",
15 | "infinite",
16 | ...(Object.keys(filters).length ? [JSON.stringify(filters)] : []),
17 | ],
18 | queryFn: ({ pageParam = 1 }) => {
19 | if (!itemId) {
20 | throw new Error("itemId is required");
21 | }
22 | return getDriver().getChildren(itemId, {
23 | page: pageParam,
24 | ...filters,
25 | });
26 | },
27 | getNextPageParam: (lastPage) => {
28 | return lastPage.pagination.hasMore
29 | ? lastPage.pagination.currentPage + 1
30 | : undefined;
31 | },
32 | initialPageParam: 1,
33 | enabled: enabled && itemId !== null,
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/modals/move/ExplorerMoveFolderModal.scss:
--------------------------------------------------------------------------------
1 | .modal__move {
2 | &__header {
3 | display: flex;
4 | flex-direction: column;
5 | gap: var(--c--globals--spacings--sm);
6 | }
7 |
8 | &__title {
9 | font-size: var(--c--globals--font--sizes--h6);
10 | font-weight: 700;
11 | color: var(--c--contextuals--content--semantic--neutral--primary);
12 | }
13 | &__description {
14 | font-size: var(--c--globals--font--sizes--body-small);
15 | color: var(--c--contextuals--content--semantic--neutral--secondary);
16 | font-weight: 400;
17 | }
18 |
19 | &__footer {
20 | width: 100%;
21 | display: flex;
22 | justify-content: space-between;
23 | align-items: center;
24 | gap: var(--c--globals--spacings--base);
25 | padding: 0 var(--c--globals--spacings--base);
26 | padding-bottom: var(--c--globals--spacings--base);
27 | }
28 |
29 | &__explorer {
30 | padding: 0 var(--c--globals--spacings--md);
31 | overflow-y: auto;
32 | }
33 | }
34 |
35 | .add-folder-icon {
36 | color: var(--c--contextuals--content--semantic--brand--tertiary);
37 | }
38 |
--------------------------------------------------------------------------------
/src/backend/core/signals.py:
--------------------------------------------------------------------------------
1 | """
2 | Declare and configure the signals for the impress core application
3 | """
4 |
5 | from functools import partial
6 |
7 | from django.db import transaction
8 | from django.db.models import signals
9 | from django.dispatch import receiver
10 |
11 | from . import models
12 | from .tasks.search import trigger_batch_file_indexer
13 |
14 |
15 | @receiver(signals.post_save, sender=models.Item)
16 | def file_post_save(sender, instance, **kwargs): # pylint: disable=unused-argument
17 | """
18 | Asynchronous call to the document indexer at the end of the transaction.
19 | Note : Within the transaction we can have an empty content and a serialization
20 | error.
21 | """
22 | transaction.on_commit(partial(trigger_batch_file_indexer, instance))
23 |
24 |
25 | @receiver(signals.post_save, sender=models.ItemAccess)
26 | def file_access_post_save(sender, instance, created, **kwargs): # pylint: disable=unused-argument
27 | """
28 | Asynchronous call to the document indexer at the end of the transaction.
29 | """
30 | if not created:
31 | transaction.on_commit(partial(trigger_batch_file_indexer, instance.item))
32 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/conf/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080;
3 | listen 3000;
4 | server_name localhost;
5 |
6 | root /usr/share/nginx/html;
7 |
8 | add_header X-Frame-Options DENY always;
9 |
10 | location / {
11 | try_files $uri index.html $uri/ =404;
12 | }
13 |
14 | location ~ "^/explorer/items/files/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" {
15 | try_files $uri /explorer/items/files/[id].html;
16 | }
17 |
18 | location ~ "^/explorer/items/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$" {
19 | try_files $uri /explorer/items/[id].html;
20 | }
21 |
22 | location ~ "^/sdk/explorer/?$" {
23 | try_files $uri /sdk/explorer.html;
24 | }
25 |
26 | location ~ "^/sdk/?$" {
27 | try_files $uri /sdk.html;
28 | }
29 |
30 | location ~ "^/explorer/trash/?$" {
31 | try_files $uri /explorer/trash.html;
32 | }
33 |
34 | location ~ "^/401/?$" {
35 | try_files $uri /401.html;
36 | }
37 |
38 | location ~ "^/403/?$" {
39 | try_files $uri /403.html;
40 | }
41 |
42 | error_page 404 /404.html;
43 | location = /404.html {
44 | internal;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/cancel.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/embedded-explorer/EmbeddedExplorerGridUpdatedAtCell.tsx:
--------------------------------------------------------------------------------
1 | import { CellContext } from "@tanstack/react-table";
2 | import { Item } from "@/features/drivers/types";
3 | import { Tooltip } from "@openfun/cunningham-react";
4 | import { timeAgo } from "@/features/explorer/utils/utils";
5 | import { Draggable } from "@/features/explorer/components/Draggable";
6 | import { useDisableDragGridItem } from "@/features/explorer/components/embedded-explorer/hooks";
7 |
8 | type EmbeddedExplorerGridUpdatedAtCellProps = CellContext
- ;
9 |
10 | export const EmbeddedExplorerGridUpdatedAtCell = (
11 | params: EmbeddedExplorerGridUpdatedAtCellProps
12 | ) => {
13 | const item = params.row.original;
14 | const disableDrag = useDisableDragGridItem(item);
15 |
16 | return (
17 |
18 |
19 |
20 | {timeAgo(new Date(item.updated_at))}
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/layouts/components/simple/SimpleLayout.tsx:
--------------------------------------------------------------------------------
1 | import { MainLayout } from "@gouvfr-lasuite/ui-kit";
2 | import { GlobalLayout } from "../global/GlobalLayout";
3 | import { HeaderRight } from "../header/Header";
4 | import { Toaster } from "@/features/ui/components/toaster/Toaster";
5 | import { LeftPanelMobile } from "@/features/layouts/components/left-panel/LeftPanelMobile";
6 |
7 | export const getSimpleLayout = (page: React.ReactElement) => {
8 | return {page};
9 | };
10 |
11 | /**
12 | * This layout is used for the simple pages.
13 | * It is used to display the header and provide
14 | * Auth context to the children.
15 | */
16 | export const SimpleLayout = ({ children }: { children: React.ReactNode }) => {
17 | return (
18 |
19 |
20 | }
24 | rightHeaderContent={}
25 | >
26 | {children}
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/backend/core/tests/swagger/test_openapi_schema.py:
--------------------------------------------------------------------------------
1 | """
2 | Test suite for generated openapi schema.
3 | """
4 |
5 | import json
6 | from io import StringIO
7 |
8 | from django.core.management import call_command
9 | from django.test import Client
10 |
11 | import pytest
12 |
13 | pytestmark = pytest.mark.django_db
14 |
15 |
16 | def test_openapi_client_schema():
17 | """
18 | Generated and served OpenAPI client schema should be correct.
19 | """
20 | # Start by generating the swagger.json file
21 | output = StringIO()
22 | call_command(
23 | "spectacular",
24 | "--api-version",
25 | "v1.0",
26 | "--urlconf",
27 | "core.urls",
28 | "--format",
29 | "openapi-json",
30 | "--file",
31 | "core/tests/swagger/swagger.json",
32 | stdout=output,
33 | )
34 | assert output.getvalue() == ""
35 |
36 | response = Client().get("/api/v1.0/swagger.json")
37 |
38 | assert response.status_code == 200
39 | with open(
40 | "core/tests/swagger/swagger.json", "r", encoding="utf-8"
41 | ) as expected_schema:
42 | assert response.json() == json.load(expected_schema)
43 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/files/icons/mime-folder-mini.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/cancel_blue.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/utils-item.ts:
--------------------------------------------------------------------------------
1 | import { expect, Page } from "@playwright/test";
2 | import { getItemTree, openTreeNode } from "./utils-tree";
3 |
4 | export const createWorkspace = async (page: Page, workspaceName: string) => {
5 | await page.getByRole("button", { name: "add Create" }).click();
6 | await page.getByRole("menuitem", { name: "New workspace" }).click();
7 | await page.getByRole("textbox", { name: "Workspace name" }).click();
8 | await page
9 | .getByRole("textbox", { name: "Workspace name" })
10 | .fill(workspaceName);
11 | await page.getByRole("button", { name: "Create" }).click();
12 | await openTreeNode(page, "Shared Space");
13 | const newWorkspaceItem = await getItemTree(page, workspaceName);
14 | await expect(newWorkspaceItem).toBeVisible();
15 | };
16 |
17 | export const createFolder = async (page: Page, folderName: string) => {
18 | await page.getByRole("button", { name: "Create Folder" }).click();
19 | await page.getByRole("textbox", { name: "Folder name" }).click();
20 | await page.getByRole("textbox", { name: "Folder name" }).fill(folderName);
21 | await page.getByRole("button", { name: "Create" }).click();
22 | };
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Direction Interministérielle du Numérique - Gouvernement Français
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/utils-explorer.ts:
--------------------------------------------------------------------------------
1 | import { expect, Page } from "@playwright/test";
2 | import { expectTreeItemIsSelected } from "./utils-tree";
3 |
4 | export const expectExplorerBreadcrumbs = async (
5 | page: Page,
6 | expected: string[],
7 | hidden: string[] = []
8 | ) => {
9 | const breadcrumbs = page.getByTestId("explorer-breadcrumbs");
10 | await expect(breadcrumbs).toBeVisible();
11 |
12 | // Check the order of breadcrumbs
13 | if (expected.length >= 1) {
14 | const breadcrumbButtons = breadcrumbs.getByTestId("breadcrumb-button");
15 |
16 | // Check each breadcrumb appears in the correct order
17 | for (let i = 0; i < expected.length; i++) {
18 | const button = breadcrumbButtons.nth(i);
19 | await expect(button).toBeVisible();
20 | await expect(button).toContainText(expected[i]);
21 | }
22 | }
23 | };
24 |
25 | export const expectCurrentFolder = async (
26 | page: Page,
27 | expected: string[],
28 | isSelected: boolean = false
29 | ) => {
30 | await expectTreeItemIsSelected(
31 | page,
32 | expected[expected.length - 1],
33 | isSelected
34 | );
35 | await expectExplorerBreadcrumbs(page, expected);
36 | };
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 | .DS_Store
30 | .next/
31 |
32 | # Translations # Translations
33 | *.mo
34 | *.pot
35 |
36 | # Environments
37 | .venv
38 | env/
39 | venv/
40 | ENV/
41 | env.bak/
42 | venv.bak/
43 | env.d/development/*.local
44 | env.d/terraform
45 |
46 | # Docker
47 | compose.override.yml
48 | docker/auth/*.local
49 |
50 | # npm
51 | node_modules
52 |
53 | # Mails
54 | src/backend/core/templates/mail/
55 |
56 | # Swagger
57 | **/swagger.json
58 |
59 | # Logs
60 | *.log
61 |
62 | # Terraform
63 | .terraform
64 | *.tfstate
65 | *.tfstate.backup
66 |
67 | # Test & lint
68 | .coverage
69 | .pylint.d
70 | .pytest_cache
71 | db.sqlite3
72 | .mypy_cache
73 |
74 | # Site media
75 | /data/
76 |
77 | # IDEs
78 | .idea/
79 | .vscode/
80 | *.iml
81 | .devcontainer
82 |
83 | # Various
84 | .turbo
85 |
--------------------------------------------------------------------------------
/src/frontend/apps/e2e/__tests__/app-drive/utils-embedded-grid.ts:
--------------------------------------------------------------------------------
1 | import { expect, Page } from "@playwright/test";
2 |
3 | export const getRowItem = async (page: Page, itemName: string) => {
4 | const row = page
5 | .getByRole("row", { name: itemName })
6 | .filter({ hasText: itemName })
7 | .first();
8 |
9 | await expect(row).toBeVisible();
10 | return row;
11 | };
12 |
13 | export const getRowItemActions = async (page: Page, itemName: string) => {
14 | const row = await getRowItem(page, itemName);
15 | const actions = row
16 | .getByRole("button", {
17 | name: `More actions for ${itemName}`,
18 | exact: true,
19 | })
20 | .nth(1);
21 | await expect(actions).toBeVisible();
22 | return actions;
23 | };
24 |
25 | export const clickOnRowItemActions = async (
26 | page: Page,
27 | itemName: string,
28 | actionName: string
29 | ) => {
30 | const actions = await getRowItemActions(page, itemName);
31 | await actions.click({ force: true }); // Because dnd-kit add an aria-disabled attribute on parent and playwright don't interact with it
32 | const action = page.getByRole("menuitem", { name: "Info" });
33 | await expect(action).toBeVisible();
34 | await action.click();
35 | };
36 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | Security is very important to us.
6 |
7 | If you have any issue regarding security, please disclose the information responsibly submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at drive@numerique.gouv.fr
8 |
9 | We appreciate your effort to make Drive more secure.
10 |
11 | ## Vulnerability disclosure policy
12 |
13 | Working with security issues in an open source project can be challenging, as we are required to disclose potential problems that could be exploited by attackers. With this in mind, our security fix policy is as follows:
14 |
15 | 1. The Maintainers team will handle the fix as usual (Pull Request,
16 | release).
17 | 2. In the release notes, we will include the identification numbers from the
18 | GitHub Advisory Database (GHSA) and, if applicable, the Common Vulnerabilities
19 | and Exposures (CVE) identifier for the vulnerability.
20 | 3. Once this grace period has passed, we will publish the vulnerability.
21 |
22 | By adhering to this security policy, we aim to address security concerns
23 | effectively and responsibly in our open source software project.
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/hooks/useDeleteItem.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | addToast,
3 | ToasterItem,
4 | } from "@/features/ui/components/toaster/Toaster";
5 | import { useMutationDeleteItems } from "./useMutations";
6 | import { useTranslation } from "react-i18next";
7 |
8 | export const useDeleteItem = () => {
9 | const { t } = useTranslation();
10 | const deleteItemsMutation = useMutationDeleteItems();
11 | const deleteItems = async (itemIds: string[]) => {
12 | try {
13 | await deleteItemsMutation.mutateAsync(itemIds);
14 | addToast(
15 |
16 | delete
17 |
18 | {t("explorer.actions.delete.toast", { count: itemIds.length })}
19 |
20 |
21 | );
22 | } catch {
23 | addToast(
24 |
25 | delete
26 |
27 | {t("explorer.actions.delete.toast_error", {
28 | count: itemIds.length,
29 | })}
30 |
31 |
32 | );
33 | }
34 | };
35 |
36 | return { deleteItems: deleteItems };
37 | };
38 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "jest";
2 | import { pathsToModuleNameMapper } from "ts-jest";
3 | import tsconfig from "./tsconfig.json";
4 |
5 | const config: Config = {
6 | preset: "ts-jest",
7 | testEnvironment: "node",
8 | roots: ["/src"],
9 | testMatch: ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.test.tsx"],
10 | moduleNameMapper: {
11 | // Handle static assets FIRST (before path aliases)
12 | "\\.(css|less|scss|sass|svg|png|jpg|jpeg|gif)$":
13 | "/__mocks__/fileMock.js",
14 | // Then handle path aliases
15 | ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, {
16 | prefix: "/",
17 | }),
18 | },
19 | transform: {
20 | "^.+\\.(ts|tsx)$": [
21 | "ts-jest",
22 | {
23 | tsconfig: {
24 | jsx: "react",
25 | moduleResolution: "node",
26 | },
27 | },
28 | ],
29 | },
30 | transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$))"],
31 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "svg"],
32 | collectCoverageFrom: [
33 | "src/**/*.{ts,tsx}",
34 | "!src/**/*.d.ts",
35 | "!src/**/__tests__/**",
36 | ],
37 | };
38 |
39 | export default config;
40 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/tree/nav/ExplorerTreeNavItem.tsx:
--------------------------------------------------------------------------------
1 | import { TreeItem } from "@/features/drivers/types";
2 | import { useTreeContext } from "@gouvfr-lasuite/ui-kit";
3 | import { useRouter } from "next/router";
4 | import { useGlobalExplorer } from "../../GlobalExplorerContext";
5 |
6 | type ExplorerTreeNavItemProps = {
7 | icon: React.ReactNode;
8 | label: string;
9 | route: string;
10 | };
11 |
12 | export const ExplorerTreeNavItem = ({
13 | icon,
14 | label,
15 | route,
16 | }: ExplorerTreeNavItemProps) => {
17 | const { setIsLeftPanelOpen } = useGlobalExplorer();
18 | const treeContext = useTreeContext();
19 | const router = useRouter();
20 | const isActive = router.pathname === route;
21 |
22 | const handleClick = () => {
23 | router.push(route);
24 | treeContext?.treeData?.setSelectedNode(undefined);
25 | setIsLeftPanelOpen(false);
26 | };
27 |
28 | return (
29 |
33 |
{icon}
34 |
{label}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/embedded-explorer/EmbeddedExplorerGridMobileCell.tsx:
--------------------------------------------------------------------------------
1 | import { CellContext } from "@tanstack/react-table";
2 | import { Item } from "@/features/drivers/types";
3 | import { ItemIcon } from "@/features/explorer/components/icons/ItemIcon";
4 | import { timeAgo } from "@/features/explorer/utils/utils";
5 | import { removeFileExtension } from "../../utils/mimeTypes";
6 | type EmbeddedExplorerGridMobileCellProps = CellContext- ;
7 |
8 | export const EmbeddedExplorerGridMobileCell = (
9 | params: EmbeddedExplorerGridMobileCellProps
10 | ) => {
11 | const item = params.row.original;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | {removeFileExtension(item.title)}
20 |
21 |
22 |
23 | {timeAgo(new Date(item.updated_at))}
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/Droppable.tsx:
--------------------------------------------------------------------------------
1 | import { Item } from "@/features/drivers/types";
2 | import { useDroppable } from "@dnd-kit/core";
3 | import {
4 | NodeRendererProps,
5 | TreeDataItem,
6 | TreeViewDataType,
7 | } from "@gouvfr-lasuite/ui-kit";
8 | import { useEffect } from "react";
9 |
10 | type DroppableProps = {
11 | id: string;
12 | disabled?: boolean;
13 | item: Item;
14 | nodeTree?: NodeRendererProps>>;
15 | children: React.ReactNode;
16 | onOver?: (isOver: boolean, fromItem: Item) => void;
17 | };
18 |
19 | export const Droppable = (props: DroppableProps) => {
20 | const { isOver, setNodeRef, active } = useDroppable({
21 | id: props.id,
22 | disabled: props.disabled,
23 | data: {
24 | item: props.item,
25 | nodeTree: props.nodeTree,
26 | },
27 | });
28 | const style = {};
29 |
30 | useEffect(() => {
31 | if (!props.disabled && props.onOver && active?.data.current?.item) {
32 | props.onOver(isOver, active?.data.current?.item as Item);
33 | }
34 | }, [isOver, props.item, active]);
35 |
36 | return (
37 |
38 | {props.children}
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/toaster/index.scss:
--------------------------------------------------------------------------------
1 | @mixin toaster-theme($color) {
2 | background-color: var(
3 | --c--contextuals--background--semantic--#{$color}--tertiary
4 | );
5 | border: 1px solid var(--c--contextuals--border--semantic--#{$color}--tertiary);
6 | color: var(--c--contextuals--content--semantic--#{$color}--primary);
7 | --toastify-color-progress-light: var(
8 | --c--contextuals--content--semantic--#{$color}--tertiary
9 | );
10 | }
11 |
12 | .suite__toaster {
13 | &__wrapper {
14 | width: auto;
15 | min-height: 0;
16 | padding: 0;
17 | }
18 |
19 | &__item {
20 | padding: 12px;
21 | border-radius: 4px;
22 | min-width: 400px;
23 | display: flex;
24 | justify-content: space-between;
25 | align-items: center;
26 | box-shadow: 0px 6px 18px 0px #0000910d;
27 | font-size: 14px;
28 | font-weight: 500;
29 | font-family: var(--c--globals--font--families--base);
30 |
31 | &__content {
32 | display: flex;
33 | gap: 8px;
34 | align-items: center;
35 | flex-grow: 1;
36 | }
37 |
38 | &--info {
39 | @include toaster-theme("brand");
40 | }
41 |
42 | &--error {
43 | @include toaster-theme("info");
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/components/icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | export const CheckIcon = () => {
2 | return (
3 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/ui/preview/components/duration-bar/DurationBar.tsx:
--------------------------------------------------------------------------------
1 | const formatTime = (time: number): string => {
2 | const minutes = Math.floor(time / 60);
3 | const seconds = Math.floor(time % 60);
4 | return `${minutes}:${seconds.toString().padStart(2, "0")}`;
5 | };
6 |
7 | type DurationBarProps = {
8 | duration: number;
9 | currentTime: number;
10 | handleSeek: (e: React.ChangeEvent) => void;
11 | };
12 |
13 | export const ProgressBar = ({
14 | duration,
15 | currentTime,
16 | handleSeek,
17 | }: DurationBarProps) => {
18 | const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;
19 |
20 | return (
21 |
22 |
{formatTime(currentTime)}
23 |
37 |
{formatTime(duration)}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/sdk/index.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from "@gouvfr-lasuite/ui-kit";
2 | import { useEffect } from "react";
3 | import { login, useAuth } from "@/features/auth/Auth";
4 | import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout";
5 | import { useSearchParams } from "next/navigation";
6 |
7 | export default function SDKPage() {
8 | const { user } = useAuth();
9 | const searchParams = useSearchParams();
10 | const mode = searchParams.get("mode");
11 | const token = searchParams.get("token");
12 |
13 | const redirect = async () => {
14 | let url = `/sdk/explorer`;
15 | if (mode) {
16 | url += `?mode=${mode}`;
17 | }
18 | window.location.href = url;
19 | };
20 |
21 | useEffect(() => {
22 | if (!token) {
23 | throw new Error("Token is required");
24 | }
25 |
26 | sessionStorage.setItem("sdk_token", token);
27 |
28 | if (user) {
29 | redirect();
30 | } else {
31 | const returnTo = window.location.href;
32 | login(returnTo);
33 | }
34 | }, [user]);
35 |
36 | return (
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | SDKPage.getLayout = function getLayout(page: React.ReactElement) {
44 | return {page};
45 | };
46 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/icons/info.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/workspace_logo.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/explorer/items/[id].tsx:
--------------------------------------------------------------------------------
1 | import { ItemFilters } from "@/features/drivers/Driver";
2 | import { AppExplorer } from "@/features/explorer/components/app-view/AppExplorer";
3 | import { getGlobalExplorerLayout } from "@/features/layouts/components/explorer/ExplorerLayout";
4 | import { useInfiniteChildren } from "@/features/explorer/hooks/useInfiniteChildren";
5 | import { useRouter } from "next/router";
6 | import { useState, useMemo } from "react";
7 | export default function ItemPage() {
8 | const router = useRouter();
9 | const itemId = router.query.id as string;
10 | const [filters, setFilters] = useState({});
11 |
12 | const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
13 | useInfiniteChildren(itemId, filters);
14 |
15 | // Flatten all pages into a single array of items
16 | const itemChildren = useMemo(() => {
17 | return data?.pages.flatMap((page) => page.children) ?? [];
18 | }, [data]);
19 |
20 | return (
21 |
30 | );
31 | }
32 |
33 | ItemPage.getLayout = getGlobalExplorerLayout;
34 |
--------------------------------------------------------------------------------
/.github/workflows/front-dependencies-installation.yml:
--------------------------------------------------------------------------------
1 | name: Install frontend installation reusable workflow
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | node_version:
7 | required: false
8 | default: '20.x'
9 | type: string
10 |
11 | jobs:
12 | front-dependencies-installation:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v6
17 | - name: Restore the frontend cache
18 | uses: actions/cache@v4
19 | id: front-node_modules
20 | with:
21 | path: "src/frontend/**/node_modules"
22 | key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
23 | - name: Setup Node.js
24 | if: steps.front-node_modules.outputs.cache-hit != 'true'
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ inputs.node_version }}
28 | - name: Install dependencies
29 | if: steps.front-node_modules.outputs.cache-hit != 'true'
30 | run: cd src/frontend/ && yarn install --frozen-lockfile
31 | - name: Cache install frontend
32 | if: steps.front-node_modules.outputs.cache-hit != 'true'
33 | uses: actions/cache@v4
34 | with:
35 | path: "src/frontend/**/node_modules"
36 | key: front-node_modules-${{ hashFiles('src/frontend/**/yarn.lock') }}
37 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/files/icons/mime-audio-mini.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/workspaces-explorer/WorkspacesExplorer.tsx:
--------------------------------------------------------------------------------
1 | import { ItemFilters } from "@/features/drivers/Driver";
2 | import { AppExplorer } from "@/features/explorer/components/app-view/AppExplorer";
3 | import { useState, useMemo } from "react";
4 | import { useInfiniteItems } from "@/features/explorer/hooks/useInfiniteItems";
5 |
6 | export type WorkspacesExplorerProps = {
7 | readonly defaultFilters: ItemFilters;
8 | readonly showFilters?: boolean;
9 | };
10 | export default function WorkspacesExplorer({
11 | defaultFilters,
12 | showFilters = true,
13 | }: WorkspacesExplorerProps) {
14 | const [filters, setFilters] = useState(defaultFilters);
15 |
16 | const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
17 | useInfiniteItems(filters);
18 |
19 | // Flatten all pages into a single array of items
20 | const itemChildren = useMemo(() => {
21 | return data?.pages.flatMap((page) => page.children) ?? [];
22 | }, [data]);
23 |
24 | return (
25 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/backend/core/api/__init__.py:
--------------------------------------------------------------------------------
1 | """Drive core API endpoints"""
2 |
3 | from django.conf import settings
4 | from django.core.exceptions import ValidationError as DjangoValidationError
5 |
6 | from drf_standardized_errors.handler import exception_handler as drf_exception_handler
7 | from rest_framework import exceptions as drf_exceptions
8 | from rest_framework.decorators import api_view
9 | from rest_framework.response import Response
10 | from rest_framework.serializers import as_serializer_error
11 |
12 |
13 | def exception_handler(exc, context):
14 | """Handle Django ValidationError as an accepted exception.
15 |
16 | For the parameters, see ``exception_handler``
17 | This code comes from twidi's gist:
18 | https://gist.github.com/twidi/9d55486c36b6a51bdcb05ce3a763e79f
19 | """
20 | if isinstance(exc, DjangoValidationError):
21 | exc = drf_exceptions.ValidationError(as_serializer_error(exc))
22 |
23 | return drf_exception_handler(exc, context)
24 |
25 |
26 | # pylint: disable=unused-argument
27 | @api_view(["GET"])
28 | def get_frontend_configuration(request):
29 | """Returns the frontend configuration dict as configured in settings."""
30 | frontend_configuration = {
31 | "LANGUAGE_CODE": settings.LANGUAGE_CODE,
32 | }
33 | frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
34 | return Response(frontend_configuration)
35 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/config/ConfigProvider.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner } from "@gouvfr-lasuite/ui-kit";
2 | import { useApiConfig } from "./useApiConfig";
3 | import { ApiConfig } from "@/features/drivers/types";
4 | import { createContext, useContext, useEffect } from "react";
5 | import { useAppContext } from "@/pages/_app";
6 |
7 | export interface ConfigContextType {
8 | config: ApiConfig;
9 | }
10 |
11 | export const ConfigContext = createContext(
12 | undefined
13 | );
14 |
15 | export const useConfig = () => {
16 | const context = useContext(ConfigContext);
17 | if (!context) {
18 | throw new Error("useConfig must be used within a ConfigProvider");
19 | }
20 | return context;
21 | };
22 |
23 | export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
24 | const { data: config } = useApiConfig();
25 | const { setTheme } = useAppContext();
26 |
27 | useEffect(() => {
28 | if (config?.FRONTEND_THEME) {
29 | setTheme(config.FRONTEND_THEME);
30 | }
31 | }, [config?.FRONTEND_THEME, setTheme]);
32 |
33 | if (!config) {
34 | return (
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | return (
42 |
43 | {children}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/tree/main-workspace.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/src/backend/e2e/viewsets.py:
--------------------------------------------------------------------------------
1 | """Viewsets for the e2e app."""
2 |
3 | from django.contrib.auth import login
4 |
5 | import rest_framework as drf
6 | from rest_framework import response as drf_response
7 | from rest_framework import status
8 | from rest_framework.permissions import AllowAny
9 |
10 | from core import models
11 |
12 | from e2e.serializers import E2EAuthSerializer
13 |
14 |
15 | class UserAuthViewSet(drf.viewsets.ViewSet):
16 | """Viewset to handle user authentication"""
17 |
18 | permission_classes = [AllowAny]
19 | authentication_classes = []
20 |
21 | def create(self, request):
22 | """
23 | POST /api/v1.0/e2e/user-auth/
24 | Create a user with the given email if it doesn't exist and log them in
25 | """
26 | serializer = E2EAuthSerializer(data=request.data)
27 | serializer.is_valid(raise_exception=True)
28 |
29 | # Create user if doesn't exist
30 | user = models.User.objects.filter(
31 | email=serializer.validated_data["email"]
32 | ).first()
33 | if not user:
34 | user = models.User(email=serializer.validated_data["email"])
35 | user.set_unusable_password()
36 | user.save()
37 |
38 | login(request, user, "django.contrib.auth.backends.ModelBackend")
39 |
40 | return drf_response.Response({"email": user.email}, status=status.HTTP_200_OK)
41 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/item-actions/ImportDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownMenu, useDropdownMenu } from "@gouvfr-lasuite/ui-kit";
2 | import uploadFileSvg from "@/assets/icons/upload_file.svg";
3 | import uploadFolderSvg from "@/assets/icons/upload_folder.svg";
4 | import { useTranslation } from "react-i18next";
5 | export type ImportDropdownProps = {
6 | trigger: React.ReactNode;
7 | importMenu: ReturnType;
8 | };
9 |
10 | export const ImportDropdown = ({
11 | trigger,
12 | importMenu,
13 | }: ImportDropdownProps) => {
14 | const { t } = useTranslation();
15 | return (
16 | ,
20 | label: t("explorer.tree.import.files"),
21 | value: "info",
22 | callback: () => {
23 | document.getElementById("import-files")?.click();
24 | },
25 | },
26 | {
27 | icon:
,
28 | label: t("explorer.tree.import.folders"),
29 | value: "info",
30 | callback: () => {
31 | document.getElementById("import-folders")?.click();
32 | },
33 | },
34 | ]}
35 | {...importMenu}
36 | onOpenChange={importMenu.setIsOpen}
37 | >
38 | {trigger}
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/backend/wopi/services/lock.py:
--------------------------------------------------------------------------------
1 | """Services for the WOPI lock operations."""
2 |
3 | from django.conf import settings
4 | from django.core.cache import cache
5 |
6 | from core.models import Item
7 |
8 |
9 | class LockService:
10 | """Service for the WOPI lock operations."""
11 |
12 | lock_timeout = settings.WOPI_LOCK_TIMEOUT
13 | lock_prefix = "wopi_lock"
14 |
15 | def __init__(self, item: Item):
16 | self.item = item
17 |
18 | @property
19 | def _lock_key(self):
20 | """Get the lock key for the item."""
21 | return f"{self.lock_prefix}:{self.item.id}"
22 |
23 | def lock(self, lock_value: str):
24 | """Lock the item."""
25 | cache.set(self._lock_key, lock_value, timeout=self.lock_timeout)
26 |
27 | def get_lock(self, default: str = None):
28 | """Get the lock."""
29 | return cache.get(self._lock_key, default)
30 |
31 | def refresh_lock(self):
32 | """Refresh the lock."""
33 | cache.touch(self._lock_key, timeout=self.lock_timeout)
34 |
35 | def is_locked(self):
36 | """Check if the item is locked."""
37 | return self.get_lock() is not None
38 |
39 | def is_lock_valid(self, lock_value: str):
40 | """Check if the lock is valid."""
41 | return cache.get(self._lock_key) == lock_value
42 |
43 | def unlock(self):
44 | """Unlock the item."""
45 | cache.delete(self._lock_key)
46 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/assets/files/icons/mime-powerpoint-mini.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/gitlint/gitlint_emoji.py:
--------------------------------------------------------------------------------
1 | """
2 | Gitlint extra rule to validate that the message title is of the form
3 | "() "
4 | """
5 | from __future__ import unicode_literals
6 |
7 | import re
8 |
9 | import requests
10 |
11 | from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
12 |
13 |
14 | class GitmojiTitle(LineRule):
15 | """
16 | This rule will enforce that each commit title is of the form "() "
17 | where gitmoji is an emoji from the list defined in https://gitmoji.carloscuesta.me and
18 | subject should be all lowercase
19 | """
20 |
21 | id = "UC1"
22 | name = "title-should-have-gitmoji-and-scope"
23 | target = CommitMessageTitle
24 |
25 | def validate(self, title, _commit):
26 | """
27 | Download the list possible gitmojis from the project's github repository and check that
28 | title contains one of them.
29 | """
30 | gitmojis = requests.get(
31 | "https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
32 | ).json()["gitmojis"]
33 | emojis = [item["emoji"] for item in gitmojis]
34 | pattern = r"^({:s})\(.*\)\s[a-zA-Z].*$".format("|".join(emojis))
35 | if not re.search(pattern, title):
36 | violation_msg = 'Title does not match regex "() "'
37 | return [RuleViolation(self.id, violation_msg, title)]
38 |
--------------------------------------------------------------------------------
/docker/files/usr/local/bin/entrypoint:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #
3 | # The container user (see USER in the Dockerfile) is an un-privileged user that
4 | # does not exists and is not created during the build phase (see Dockerfile).
5 | # Hence, we use this entrypoint to wrap commands that will be run in the
6 | # container to create an entry for this user in the /etc/passwd file.
7 | #
8 | # The following environment variables may be passed to the container to
9 | # customize running user account:
10 | #
11 | # * USER_NAME: container user name (default: default)
12 | # * HOME : container user home directory (default: none)
13 | #
14 | # To pass environment variables, you can either use the -e option of the docker run command:
15 | #
16 | # docker run --rm -e USER_NAME=foo -e HOME='/home/foo' drive-backend:latest python manage.py migrate
17 | #
18 | # or define new variables in an environment file to use with docker or docker compose:
19 | #
20 | # # env.d/production
21 | # USER_NAME=foo
22 | # HOME=/home/foo
23 | #
24 | # docker run --rm --env-file env.d/production drive-backend:latest python manage.py migrate
25 | #
26 |
27 | echo "🐳(entrypoint) creating user running in the container..."
28 | if ! whoami > /dev/null 2>&1; then
29 | if [ -w /etc/passwd ]; then
30 | echo "${USER_NAME:-default}:x:$(id -u):$(id -g):${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd
31 | fi
32 | fi
33 |
34 | echo "🐳(entrypoint) running your command: ${*}"
35 | exec "$@"
36 |
--------------------------------------------------------------------------------
/src/frontend/packages/sdk/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/frontend/apps/sdk-consumer/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bin/scalingo_pgdump.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | mkdir -p /tmp/pgdump
6 | cd /tmp/pgdump
7 |
8 | RESTIC_VERSION=0.18.0
9 | curl -fsSL --remote-name-all "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2" \
10 | "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/SHA256SUMS" \
11 | "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/SHA256SUMS.asc"
12 |
13 | curl -fsSLo - https://restic.net/gpg-key-alex.asc | gpg --import
14 | gpg --verify SHA256SUMS.asc SHA256SUMS
15 | grep _linux_amd64.bz2 SHA256SUMS | sha256sum -c
16 | bzip2 -d restic_${RESTIC_VERSION}_linux_amd64.bz2
17 | mv restic_${RESTIC_VERSION}_linux_amd64 restic
18 | chmod +x ./restic
19 |
20 | # Download postgresql client binaries
21 | dbclient-fetcher pgsql
22 |
23 | # Actually dump and upload to scaleway s3
24 | FILENAME="${APP}_pgdump.sql"
25 |
26 | pg_dump --clean --if-exists --format c --dbname "${SCALINGO_POSTGRESQL_URL}" --no-owner --no-privileges --no-comments --exclude-schema 'information_schema' --exclude-schema '^pg_*' --file "${FILENAME}"
27 |
28 | export AWS_ACCESS_KEY_ID=${BACKUP_PGSQL_S3_KEY}
29 | export AWS_SECRET_ACCESS_KEY=${BACKUP_PGSQL_S3_SECRET}
30 | export RESTIC_PASSWORD=${BACKUP_PGSQL_ENCRYPTION_PASS}
31 | export RESTIC_REPOSITORY=s3:${BACKUP_PGSQL_S3_REPOSITORY}/${APP}
32 |
33 | ./restic snapshots -q || ./restic init
34 |
35 | ./restic backup ${FILENAME}
36 | ./restic forget --keep-daily 30
37 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/pages/sdk/index.scss:
--------------------------------------------------------------------------------
1 | .sdk__page {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 100vh;
6 | }
7 |
8 | .sdk__explorer {
9 | padding: 0 var(--c--globals--spacings--md);
10 | flex-grow: 1;
11 | display: flex;
12 | overflow-y: auto;
13 |
14 | &__page {
15 | height: calc(100vh - 50px);
16 | display: flex;
17 | flex-direction: column;
18 | }
19 |
20 | .embedded-explorer {
21 | flex-grow: 1;
22 | }
23 |
24 | .explorer__grid__item__name__text {
25 | display: flex;
26 | align-items: center;
27 | gap: 8px;
28 | }
29 | }
30 |
31 | .sdk__explorer__header {
32 | height: 50px;
33 | display: flex;
34 | align-items: center;
35 | padding: var(--c--globals--spacings--base) var(--c--globals--spacings--base) 0
36 | var(--c--globals--spacings--base);
37 | font-size: 14px;
38 | color: var(--c--contextuals--content--semantic--neutral--secondary);
39 | }
40 |
41 | .sdk__explorer__footer {
42 | display: flex;
43 | justify-content: space-between;
44 | align-items: center;
45 | position: sticky;
46 | bottom: 0;
47 | padding: 0 24px;
48 | background-color: white;
49 | border-top: 1px solid #e5e7eb;
50 | height: 72px;
51 | z-index: 10;
52 | flex-shrink: 0;
53 |
54 | &__caption {
55 | font-size: 14px;
56 | color: var(--c--contextuals--content--semantic--neutral--secondary);
57 | }
58 |
59 | &__actions {
60 | display: flex;
61 | gap: 12px;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/frontend/apps/drive/src/features/explorer/components/app-view/ExplorerSearchButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, useModal } from "@openfun/cunningham-react";
2 | import { ExplorerSearchModal } from "@/features/explorer/components/modals/search/ExplorerSearchModal";
3 | import { useTranslation } from "react-i18next";
4 | import { useEffect } from "react";
5 | import { ItemFilters } from "@/features/drivers/Driver";
6 | export const ExplorerSearchButton = ({
7 | keyboardShortcut,
8 | defaultFilters,
9 | }: {
10 | keyboardShortcut?: boolean;
11 | defaultFilters?: ItemFilters;
12 | }) => {
13 | const searchModal = useModal();
14 | const { t } = useTranslation();
15 |
16 | // Toggle the menu when ⌘K is pressed
17 | useEffect(() => {
18 | if (!keyboardShortcut) {
19 | return;
20 | }
21 | const down = (e: KeyboardEvent) => {
22 | if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
23 | e.preventDefault();
24 | searchModal.open();
25 | }
26 | };
27 |
28 | document.addEventListener("keydown", down);
29 | return () => document.removeEventListener("keydown", down);
30 | }, [keyboardShortcut]);
31 |
32 | return (
33 | <>
34 |
35 |
36 |