nixos/lasuite-drive: init (#409878)

This commit is contained in:
Martin Weinelt
2026-05-23 23:06:47 +00:00
committed by GitHub
13 changed files with 1214 additions and 0 deletions
@@ -132,6 +132,8 @@
- [Howdy](https://github.com/boltgolt/howdy), a Windows Hello™ style facial authentication program for Linux.
- [SuiteNumérique Drive](https://github.com/suitenumerique/drive), a collaborative file sharing and document management platform that scales. Built with Django and React. Open source alternative to Sharepoint or Google Drive.
- [linux-enable-ir-emitter](https://github.com/EmixamPP/linux-enable-ir-emitter), a tool used to set up IR cameras, used with Howdy.
- [udp-over-tcp](https://github.com/mullvad/udp-over-tcp), a tunnel for proxying UDP traffic over a TCP stream. Available as `services.udp-over-tcp`.
+1
View File
@@ -1697,6 +1697,7 @@
./services/web-apps/komga.nix
./services/web-apps/lanraragi.nix
./services/web-apps/lasuite-docs.nix
./services/web-apps/lasuite-drive.nix
./services/web-apps/lasuite-meet.nix
./services/web-apps/lauti.nix
./services/web-apps/lemmy.nix
@@ -0,0 +1,542 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
inherit (lib)
concatMapStringsSep
concatStringsSep
escapeShellArg
getExe
hasSuffix
mapAttrs
match
mkEnableOption
mkIf
mkPackageOption
mkOption
types
optional
optionalString
;
cfg = config.services.lasuite-drive;
pythonEnvironment = mapAttrs (
_: value:
if value == null then
"None"
else if value == true then
"True"
else if value == false then
"False"
else
toString value
) cfg.settings;
commonServiceConfig = {
RuntimeDirectory = "lasuite-drive";
StateDirectory = "lasuite-drive";
WorkingDirectory = "/var/lib/lasuite-drive";
User = "lasuite-drive";
DynamicUser = true;
SupplementaryGroups = mkIf cfg.redis.createLocally [
config.services.redis.servers.lasuite-drive.group
];
# hardening
AmbientCapabilities = "";
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
MemoryDenyWriteExecute = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0077";
EnvironmentFile = cfg.environmentFiles;
};
proxySuffix = if match "unix:.*" cfg.bind != null then ":" else "";
# Convert environment variables to be used as systemd-run arguments
envArgs = lib.concatStringsSep " " (
lib.mapAttrsToList (name: value: "-E ${escapeShellArg "${name}=${value}"}") pythonEnvironment
);
# Easier usage of django manage.py stuff
manage = pkgs.writeShellScriptBin "lasuite-drive-manage" ''
exec ${lib.getExe' config.systemd.package "systemd-run"} \
-p User=${commonServiceConfig.User} \
-p DynamicUser=yes \
-p StateDirectory=${commonServiceConfig.StateDirectory} \
${optionalString cfg.redis.createLocally "-p SupplementaryGroups=${config.services.redis.servers.lasuite-drive.group} \\"}
${concatMapStringsSep "\n" (envFile: "-p EnvironmentFile=${envFile} \\") cfg.environmentFiles}
--working-directory=${commonServiceConfig.WorkingDirectory} \
--quiet --collect --pipe --pty \
${envArgs} ${lib.getExe cfg.package} "$@"
'';
in
{
options.services.lasuite-drive = {
enable = mkEnableOption "SuiteNumérique Drive";
package = mkPackageOption pkgs "lasuite-drive" { };
bind = mkOption {
type = types.str;
default = "unix:/run/lasuite-drive/gunicorn.sock";
example = "127.0.0.1:8000";
description = ''
The path, host/port or file descriptior to bind the gunicorn socket to.
See <https://docs.gunicorn.org/en/stable/settings.html#bind> for possible options.
'';
};
enableNginx = mkEnableOption "enable and configure Nginx for reverse proxying" // {
default = true;
};
secretKeyPath = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to the Django secret key.
The key can be generated using:
```
python3 -c 'import secrets; print(secrets.token_hex())'
```
:::{.note}
If not specified, a secret key is automatically generated and stored in the state directory.
:::
'';
};
s3Url = mkOption {
type = types.str;
description = ''
URL of the S3 bucket.
'';
};
postgresql = {
createLocally = mkOption {
type = types.bool;
default = false;
description = ''
Configure local PostgreSQL database server for drive.
'';
};
};
redis = {
createLocally = mkOption {
type = types.bool;
default = false;
description = ''
Configure local Redis cache server for drive.
'';
};
};
gunicorn = {
extraArgs = mkOption {
type = types.listOf types.str;
default = [
"--name=drive"
"--workers=3"
];
description = ''
Extra arguments to pass to the gunicorn process.
'';
};
};
celery = {
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra arguments to pass to the celery process.
'';
};
};
domain = mkOption {
type = types.str;
description = ''
Domain name of the drive instance.
'';
};
settings = mkOption {
type = types.submodule {
freeformType = types.attrsOf (
types.nullOr (
types.oneOf [
types.str
types.bool
types.path
types.int
]
)
);
options = {
DJANGO_CONFIGURATION = mkOption {
type = types.str;
internal = true;
default = "Production";
description = "The configuration that Django will use";
};
DJANGO_SETTINGS_MODULE = mkOption {
type = types.str;
internal = true;
default = "drive.settings";
description = "The configuration module that Django will use";
};
DJANGO_SECRET_KEY_FILE = mkOption {
type = types.path;
default =
if cfg.secretKeyPath == null then "/var/lib/lasuite-drive/django_secret_key" else cfg.secretKeyPath;
description = "The path to the file containing Django's secret key";
};
DATA_DIR = mkOption {
type = types.path;
default = "/var/lib/lasuite-drive";
description = "Path to the data directory";
readOnly = true;
};
DJANGO_ALLOWED_HOSTS = mkOption {
type = types.listOf types.str;
default =
if cfg.enableNginx then
[
"localhost"
"127.0.0.1"
cfg.domain
]
else
[ ];
defaultText = lib.literalExpression ''
if cfg.enableNginx then [ "localhost" "127.0.0.1" cfg.domain ] else [ ]
'';
apply = list: concatStringsSep "," list;
description = "Comma-separated list of hosts that are able to connect to the server";
};
DB_NAME = mkOption {
type = types.str;
default = "lasuite-drive";
description = "Name of the database";
};
DB_USER = mkOption {
type = types.str;
default = "lasuite-drive";
description = "User of the database";
};
DB_HOST = mkOption {
type = types.nullOr types.str;
default = if cfg.postgresql.createLocally then "/run/postgresql" else null;
description = "Host of the database";
};
REDIS_URL = mkOption {
type = types.nullOr types.str;
default =
if cfg.redis.createLocally then
"unix://${config.services.redis.servers.lasuite-drive.unixSocket}?db=1"
else
null;
description = "URL of the redis backend";
};
CELERY_BROKER_URL = mkOption {
type = types.nullOr types.str;
default =
if cfg.redis.createLocally then
"redis+socket://${config.services.redis.servers.lasuite-drive.unixSocket}?db=2"
else
null;
description = "URL of the redis backend for celery";
};
};
};
default = { };
example = ''
{
AWS_S3_ENDPOINT_URL = "https://s3.us-west.amazonaws.com";
}
'';
description = ''
Configuration options of drive.
See <https://github.com/suitenumerique/drive/blob/v${cfg.package.version}/docs/env.md>
`REDIS_URL` and `CELERY_BROKER_URL` are set if `services.lasuite-drive.redis.createLocally` is true.
`DB_HOST` is set if `services.lasuite-drive.postgresql.createLocally` is true.
'';
};
environmentFiles = mkOption {
type = types.listOf types.path;
default = [ ];
description = ''
Path to environment files.
This can be useful to pass secrets to drive via tools like `agenix` or `sops`.
'';
};
};
config = mkIf cfg.enable {
warnings = mkIf (cfg.enableNginx && !(hasSuffix "/" cfg.s3Url)) [
''
services.lasuite-drive.s3Url should end with a trailing slash (/).
This could break the HTTP requests by nginx to the S3 backend.
''
];
environment.systemPackages = [ manage ];
systemd.services.lasuite-drive = {
description = "Drive from SuiteNumérique";
after = [
"network-online.target"
]
++ (optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wants =
(optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wantedBy = [ "multi-user.target" ];
preStart = ''
if [ ! -f .version ]; then
touch .version
fi
${optionalString (cfg.secretKeyPath == null) ''
if [[ ! -f /var/lib/lasuite-drive/django_secret_key ]]; then
(
umask 0377
tr -dc A-Za-z0-9 < /dev/urandom | head -c64 | ${pkgs.moreutils}/bin/sponge /var/lib/lasuite-drive/django_secret_key
)
fi
''}
if [ "${cfg.package.version}" != "$(cat .version)" ]; then
${getExe cfg.package} migrate
echo -n "${cfg.package.version}" > .version
fi
'';
environment = pythonEnvironment;
serviceConfig = {
BindReadOnlyPaths = "${cfg.package}/share/static:/var/lib/lasuite-drive/static";
ExecStart = utils.escapeSystemdExecArgs (
[
(lib.getExe' cfg.package "gunicorn")
"--bind=${cfg.bind}"
]
++ cfg.gunicorn.extraArgs
++ [ "drive.wsgi:application" ]
);
}
// commonServiceConfig;
};
systemd.services.lasuite-drive-celery = {
description = "Docs Celery broker from SuiteNumérique";
after = [
"network-online.target"
]
++ (optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wants =
(optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wantedBy = [ "multi-user.target" ];
environment = pythonEnvironment;
serviceConfig = {
ExecStart = utils.escapeSystemdExecArgs (
[ (lib.getExe' cfg.package "celery") ]
++ cfg.celery.extraArgs
++ [
"--app=drive.celery_app"
"worker"
]
);
}
// commonServiceConfig;
};
systemd.services.lasuite-drive-beat = {
description = "Docs Celery beat from SuiteNumérique";
after = [
"network-online.target"
]
++ (optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wants =
(optional cfg.postgresql.createLocally "postgresql.service")
++ (optional cfg.redis.createLocally "redis-lasuite-drive.service");
wantedBy = [ "multi-user.target" ];
environment = pythonEnvironment;
serviceConfig = {
ExecStart = utils.escapeSystemdExecArgs (
[ (lib.getExe' cfg.package "celery") ]
++ cfg.celery.extraArgs
++ [
"--app=drive.celery_app"
"beat"
]
);
}
// commonServiceConfig;
};
services.postgresql = mkIf cfg.postgresql.createLocally {
enable = true;
ensureDatabases = [ "lasuite-drive" ];
ensureUsers = [
{
name = "lasuite-drive";
ensureDBOwnership = true;
}
];
};
services.redis.servers.lasuite-drive = mkIf cfg.redis.createLocally { enable = true; };
services.nginx = mkIf cfg.enableNginx {
enable = true;
virtualHosts.${cfg.domain} = {
extraConfig = ''
error_page 401 /401.html;
error_page 403 /403.html;
error_page 404 /404.html;
'';
root = cfg.package.frontend;
locations."/" = {
tryFiles = "$uri $uri.html index.html $uri/ =404";
};
locations."~ '^/explorer/items/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$'" =
{
tryFiles = "$uri /explorer/items/[id].html";
};
locations."~ '^/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}/?$'" =
{
tryFiles = "$uri /explorer/items/files/[id].html";
};
locations."~ '^/wopi/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/?$'" = {
tryFiles = "$uri /wopi/[id].html";
};
locations."/static/" = {
alias = "${cfg.package}/share/static/";
};
locations."/api" = {
proxyPass = "http://${cfg.bind}";
recommendedProxySettings = true;
};
locations."/admin" = {
proxyPass = "http://${cfg.bind}";
recommendedProxySettings = true;
};
locations."/media-auth" = {
proxyPass = "http://${cfg.bind}${proxySuffix}/api/v1.0/items/media-auth/";
recommendedProxySettings = true;
extraConfig = ''
proxy_set_header X-Original-URL $request_uri;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-Method $request_method;
'';
};
locations."/media/" = {
proxyPass = cfg.s3Url;
extraConfig = ''
auth_request /media-auth;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $authDate $upstream_http_x_amz_date;
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
proxy_set_header Authorization $authHeader;
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
add_header Content-Disposition "attachment";
'';
};
locations."/media/preview/" = {
proxyPass = cfg.s3Url;
extraConfig = ''
auth_request /media-auth;
auth_request_set $authHeader $upstream_http_authorization;
auth_request_set $authDate $upstream_http_x_amz_date;
auth_request_set $authContentSha256 $upstream_http_x_amz_content_sha256;
proxy_set_header Authorization $authHeader;
proxy_set_header X-Amz-Date $authDate;
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
'';
};
};
};
};
meta = {
buildDocsInSandbox = false;
maintainers = [ lib.maintainers.soyouzpanda ];
};
}
+1
View File
@@ -872,6 +872,7 @@ in
languagetool = runTest ./languagetool.nix;
lanraragi = runTest ./lanraragi.nix;
lasuite-docs = runTest ./web-apps/lasuite-docs.nix;
lasuite-drive = runTest ./web-apps/lasuite-drive.nix;
lasuite-meet = runTest ./web-apps/lasuite-meet.nix;
latestKernel.login = runTest {
imports = [ ./login.nix ];
+243
View File
@@ -0,0 +1,243 @@
{ lib, ... }:
let
domain = "drive.local";
oidcAddr = "127.0.0.1:8080";
s3Addr = "127.0.0.1:9000";
garageAccessKey = "GKaaaaaaaaaaaaaaaaaaaaaaaa";
garageSecretKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
in
{
name = "lasuite-drive";
meta.maintainers = with lib.maintainers; [
soyouzpanda
];
containers.machine =
{ pkgs, ... }:
{
networking.hosts."127.0.0.1" = [ domain ];
environment.systemPackages = with pkgs; [
awscli2
jq
];
services.lasuite-drive = {
enable = true;
enableNginx = true;
redis.createLocally = true;
postgresql.createLocally = true;
inherit domain;
s3Url = "http://${s3Addr}/lasuite-drive/";
settings = {
DJANGO_SECRET_KEY_FILE = pkgs.writeText "django-secret-file" ''
8540db59c03943d48c3ed1a0f96ce3b560e0f45274f120f7ee4dace3cc366a6b
'';
OIDC_OP_JWKS_ENDPOINT = "http://${oidcAddr}/dex/keys";
OIDC_OP_AUTHORIZATION_ENDPOINT = "http://${oidcAddr}/dex/auth/mock";
OIDC_OP_TOKEN_ENDPOINT = "http://${oidcAddr}/dex/token";
OIDC_OP_USER_ENDPOINT = "http://${oidcAddr}/dex/userinfo";
OIDC_RP_CLIENT_ID = "lasuite-drive";
OIDC_RP_SIGN_ALGO = "RS256";
OIDC_RP_SCOPES = "openid email";
OIDC_RP_CLIENT_SECRET = "lasuitedriveclientsecret";
LOGIN_REDIRECT_URL = "http://${domain}";
LOGIN_REDIRECT_URL_FAILURE = "http://${domain}";
LOGOUT_REDIRECT_URL = "http://${domain}";
AWS_S3_ENDPOINT_URL = "http://${s3Addr}";
AWS_S3_ACCESS_KEY_ID = garageAccessKey;
AWS_S3_SECRET_ACCESS_KEY = garageSecretKey;
AWS_STORAGE_BUCKET_NAME = "lasuite-drive";
AWS_S3_REGION_NAME = "garage";
MEDIA_BASE_URL = "http://${domain}";
# Disable HTTPS feature in tests because we're running on a HTTP connection
DJANGO_SECURE_PROXY_SSL_HEADER = "";
DJANGO_SECURE_SSL_REDIRECT = false;
DJANGO_CSRF_COOKIE_SECURE = false;
DJANGO_SESSION_COOKIE_SECURE = false;
DJANGO_CSRF_TRUSTED_ORIGINS = "http://*";
};
};
services.dex = {
enable = true;
settings = {
issuer = "http://${oidcAddr}/dex";
storage = {
type = "postgres";
config.host = "/var/run/postgresql";
};
web.http = "127.0.0.1:8080";
oauth2.skipApprovalScreen = true;
staticClients = [
{
id = "lasuite-drive";
name = "Drive";
redirectURIs = [ "http://${domain}/api/v1.0/callback/" ];
secretFile = "/etc/dex/lasuite-drive";
}
];
connectors = [
{
type = "mockPassword";
id = "mock";
name = "Example";
config = {
username = "admin";
password = "password";
};
}
];
};
};
services.garage = {
enable = true;
package = pkgs.garage_2;
settings = {
rpc_bind_addr = "127.0.0.1:3901";
rpc_public_addr = "127.0.0.1:3901";
rpc_secret = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
replication_factor = 1;
s3_api = {
s3_region = "garage";
api_bind_addr = s3Addr;
};
};
};
environment.etc."dex/lasuite-drive" = {
mode = "0400";
user = "dex";
text = "lasuitedriveclientsecret";
};
services.postgresql = {
enable = true;
ensureDatabases = [ "dex" ];
ensureUsers = [
{
name = "dex";
ensureDBOwnership = true;
}
];
};
};
testScript = ''
import json
with subtest("Wait for units to start"):
machine.wait_for_unit("dex.service")
machine.wait_for_unit("garage.service")
machine.wait_for_unit("lasuite-drive.service")
machine.wait_for_unit("lasuite-drive-celery.service")
machine.wait_for_unit("lasuite-drive-beat.service")
with subtest("Create S3 bucket"):
machine.wait_for_open_port(3901)
garage_node_id = machine.succeed("garage status | tail -n1 | awk '{ print $1 }'")
machine.succeed(f"garage layout assign -c 100MB -z garage {garage_node_id}")
machine.succeed("garage layout apply --version 1")
machine.succeed("garage key import ${garageAccessKey} ${garageSecretKey} --yes")
machine.succeed("garage bucket create lasuite-drive")
machine.succeed("garage bucket allow --read --write --owner lasuite-drive --key ${garageAccessKey}")
machine.succeed("AWS_SECRET_ACCESS_KEY=${garageSecretKey} "
"AWS_ACCESS_KEY_ID=${garageAccessKey} "
"aws --endpoint http://${s3Addr} "
"s3api put-bucket-cors "
"--bucket lasuite-drive --cors-configuration "
"""'{"CORSRules":[{"AllowedHeaders":["*"],"AllowedMethods":["PUT"],"AllowedOrigins":["http://${domain}"]}]}'""")
with subtest("Wait for web servers to start"):
machine.wait_until_succeeds("curl -fs "
"'http://${domain}/api/v1.0/authenticate/'",
timeout=120)
machine.wait_until_succeeds("curl -fs "
"'${oidcAddr}/dex/auth/mock?client_id=lasuite-drive&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid'",
timeout=120)
with subtest("Login"):
state, nonce = machine.succeed("curl -fs -c cjar "
"'http://${domain}/api/v1.0/authenticate/' "
"-w '%{redirect_url}' "
"| sed -n 's/.*state=\\(.*\\)&nonce=\\(.*\\)/\\1 \\2/p'").strip().split(' ')
oidc_state = machine.succeed("curl -fs "
f"'${oidcAddr}/dex/auth/mock?client_id=lasuite-drive&response_type=code&redirect_uri=http://${domain}/api/v1.0/callback/&scope=openid+email&state={state}&nonce={nonce}' "
"| sed -n 's/.*state=\\(.*\\)\">.*/\\1/p'").strip()
code = machine.succeed("curl -fs "
f"'${oidcAddr}/dex/auth/mock/login?back=&state={oidc_state}' "
"-d 'login=admin&password=password' "
"-w '%{redirect_url}' "
"| sed -n 's/.*code=\\(.*\\)&.*/\\1/p'").strip()
print(f"Got approval code {code}")
machine.succeed(f"curl -fs -c cjar -b cjar 'http://${domain}/api/v1.0/callback/?code={code}&state={state}'")
with subtest("Upload a document"):
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
upload = json.loads(
machine.succeed("curl -fs -c cjar -b cjar "
"'http://${domain}/api/v1.0/items/' "
"-X POST "
f"-H 'X-CSRFToken: {csrf_token}' "
"-H 'Content-Type: application/json' "
"-H 'Referer: http://${domain}/explorer/items/my-files' "
"--data-raw '{\"type\":\"file\",\"filename\":\"text.txt\"}'"
)
)
print(f"Created file with id {upload['id']}")
machine.succeed("curl -fs "
f"'{upload['policy']}' "
"-X PUT "
"-H 'Origin: http://${domain}' "
"-H 'X-amz-acl: private' "
"-H 'Content-Type: text/plain' "
"-H 'Content-Length: 8' "
"--data-raw 'sometext'")
print("Uploaded file")
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
machine.succeed("curl -fs -c cjar -b cjar "
f"'http://${domain}/api/v1.0/items/{upload['id']}/upload-ended/' "
"-X POST "
f"-H 'X-CSRFToken: {csrf_token}' "
"-H 'Referer: http://${domain}/explorer/items/my-files'")
with subtest("Download a document"):
csrf_token = machine.succeed("grep csrftoken cjar | cut -f 7 | tr -d '\n'")
items = json.loads(
machine.succeed("curl -fs -c cjar -b cjar "
f"'http://${domain}/api/v1.0/items/' "
"-X GET "
f"-H 'X-CSRFToken: {csrf_token}' "
"-H 'Referer: http://${domain}/explorer/items/my-files'"
)
)
assert items["count"] == 1
url = items["results"][0]["url_permalink"]
data = machine.succeed("curl -fs -c cjar -b cjar -L "
f"'{url}' "
"-X GET "
"-H 'Referer: http://${domain}/explorer/items/my-files'")
assert data == "sometext"
'';
}
@@ -0,0 +1,48 @@
{
src,
version,
meta,
stdenv,
fetchYarnDeps,
nodejs,
fixup-yarn-lock,
yarn,
yarnConfigHook,
yarnBuildHook,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "lasuite-drive-frontend";
inherit src version;
sourceRoot = "${finalAttrs.src.name}/src/frontend";
offlineCache = fetchYarnDeps {
yarnLock = "${finalAttrs.src}/src/frontend/yarn.lock";
hash = "sha256-yUKJp6yUTxpvkaA+YuQC3r1t4LBvuYMv1xesLewbK/U=";
};
nativeBuildInputs = [
nodejs
fixup-yarn-lock
yarn
yarnConfigHook
yarnBuildHook
];
strictDeps = true;
installPhase = ''
runHook preInstall
cp -r apps/drive/out/ $out
runHook postInstall
'';
__structuredAttrs = true;
meta = meta // {
description = "A collaborative file sharing and document management platform that scales. Built with Django and React. Opensource alternative to Sharepoint or Google Drive";
};
})
+42
View File
@@ -0,0 +1,42 @@
{
src,
version,
meta,
stdenv,
fetchYarnDeps,
nodejs,
yarnConfigHook,
yarnBuildHook,
}:
stdenv.mkDerivation (finalAttrs: {
pname = "lasuite-drive-mail";
inherit src version;
sourceRoot = "${finalAttrs.src.name}/src/mail";
postPatch = ''
substituteInPlace bin/html-to-plain-text bin/mjml-to-html \
--replace-fail \
'../backend/core/templates/mail' \
'${placeholder "out"}'
'';
offlineCache = fetchYarnDeps {
yarnLock = "${finalAttrs.src}/src/mail/yarn.lock";
hash = "sha256-UPIb9QJk+zC8wYeBeDnmlGLhHDhsEOoT+qquFM1XyqU=";
};
nativeBuildInputs = [
nodejs
yarnConfigHook
yarnBuildHook
];
dontInstall = true;
__structuredAttrs = true;
meta = meta // {
description = "HTML mail templates for LaSuite Drive";
};
})
+156
View File
@@ -0,0 +1,156 @@
{
callPackage,
lib,
python3,
stdenv,
fetchFromGitHub,
nixosTests,
}:
let
version = "0.18.0";
src = fetchFromGitHub {
owner = "suitenumerique";
repo = "drive";
tag = "v${version}";
hash = "sha256-JoOHbwZR4salfLB9Gg7kfRMDcDA/Srn8qwUqLAZtsz8=";
};
meta = {
homepage = "https://github.com/suitenumerique/drive";
changelog = "https://github.com/suitenumerique/drive/blob/${src.tag}/CHANGELOG.md";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ soyouzpanda ];
platforms = lib.platforms.linux;
};
mail = callPackage ./mail.nix { inherit src version meta; };
frontend = callPackage ./frontend.nix { inherit src version meta; };
python = python3.override {
self = python;
packageOverrides = (self: super: { django = super.django_5; });
};
in
python.pkgs.buildPythonApplication (finalAttrs: {
pname = "lasuite-drive";
pyproject = true;
inherit version src;
sourceRoot = "${finalAttrs.src.name}/src/backend";
patches = [
# Support configuration throught environment variables for SECURE_*
./secure_settings.patch
# Fix some build fields on pyproject
./pyproject_build.patch
];
build-system = with python.pkgs; [ uv-build ];
dependencies =
with python.pkgs;
[
boto3
brotli
celery
defusedxml
dj-database-url
django
django-configurations
django-cors-headers
django-countries
django-debug-toolbar
django-extensions
django-filter
django-lasuite
django-ltree
django-parler
django-pydantic-field
django-redis
django-storages
django-timezone-field
djangorestframework
djangorestframework-api-key
dockerflow
drf-spectacular
drf-spectacular-sidecar
drf-standardized-errors
easy-thumbnails
factory-boy
gunicorn
jsonschema
markdown
mozilla-django-oidc
nested-multipart-parser
posthog
psycopg
pydantic
pyjwt
python-magic
redis
requests
sentry-sdk
url-normalize
whitenoise
]
++ celery.optional-dependencies.redis
++ django-storages.optional-dependencies.s3;
pythonRelaxDeps = true;
postPatch = ''
# Put assets inside a data directory
# so uv will copy the assets directory
# entirely
mkdir data
mv assets data
''
+ (lib.optionalString stdenv.hostPlatform.isDarwin ''
substituteInPlace impress/settings.py \
--replace-fail \
"gethostname()" \
"gethostname() + '.local'"
'');
__darwinAllowLocalNetworking = true;
postBuild = ''
export DATA_DIR=$(pwd)/data
${python.pythonOnBuildForHost.interpreter} manage.py collectstatic --no-input --clear
'';
postInstall =
let
pythonPath = python.pkgs.makePythonPath finalAttrs.passthru.dependencies;
in
''
mkdir -p $out/{bin,share}
cp ./manage.py $out/bin/.manage.py
cp -r data/static $out/share
chmod +x $out/bin/.manage.py
makeWrapper $out/bin/.manage.py $out/bin/drive \
--prefix PYTHONPATH : "${pythonPath}"
makeWrapper ${lib.getExe python.pkgs.celery} $out/bin/celery \
--prefix PYTHONPATH : "${pythonPath}:$out/${python.sitePackages}"
makeWrapper ${lib.getExe python.pkgs.gunicorn} $out/bin/gunicorn \
--prefix PYTHONPATH : "${pythonPath}:$out/${python.sitePackages}"
mkdir -p $out/${python.sitePackages}/core/templates
ln -sv ${mail}/ $out/${python.sitePackages}/core/templates/mail
'';
passthru = {
inherit mail frontend;
tests = {
login-upload-and-download-file = nixosTests.lasuite-drive;
};
};
__structuredAttrs = true;
meta = meta // {
description = "A collaborative file sharing and document management platform that scales. Built with Django and React. Opensource alternative to Sharepoint or Google Drive";
mainProgram = "drive";
};
})
@@ -0,0 +1,24 @@
diff --git a/pyproject.toml b/pyproject.toml
index ba8f5362..ddc40c83 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,7 @@
# drive package
#
[build-system]
-requires = ["uv_build>=0.11.7,<0.12.0"]
+requires = ["uv_build"]
build-backend = "uv_build"
[project]
@@ -102,6 +102,9 @@ constraint-dependencies = [
[tool.uv.build-backend]
module-root = ""
+module-name = ["core", "demo", "drive", "e2e", "wopi"]
+data = ["data"]
+namespace = true
source-exclude = [
"**/tests/**",
"**/test_*.py",
@@ -0,0 +1,36 @@
diff --git a/drive/settings.py b/drive/settings.py
index 82f4d31..3d66c23 100755
--- a/drive/settings.py
+++ b/drive/settings.py
@@ -775,19 +775,24 @@ class Production(Base):
#
# In other cases, you should comment the following line to avoid security issues.
# SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
- SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
- SECURE_HSTS_SECONDS = 60
- SECURE_HSTS_PRELOAD = True
- SECURE_HSTS_INCLUDE_SUBDOMAINS = True
- SECURE_SSL_REDIRECT = True
+ SECURE_PROXY_SSL_HEADER = values.TupleValue(("HTTP_X_FORWARDED_PROTO", "https"),
+ environ_name="SECURE_PROXY_SSL_HEADER")
+ SECURE_HSTS_SECONDS = values.IntegerValue(
+ 60, environ_name="SECURE_HSTS_SECONDS")
+ SECURE_HSTS_PRELOAD = values.BooleanValue(
+ True, environ_name="SECURE_HSTS_PRELOAD")
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = values.BooleanValue(
+ True, environ_name="SECURE_HSTS_INCLUDE_SUBDOMAINS")
+ SECURE_SSL_REDIRECT = values.BooleanValue(
+ True, environ_name="SECURE_SSL_REDIRECT")
SECURE_REDIRECT_EXEMPT = [
"^__lbheartbeat__",
"^__heartbeat__",
]
# Modern browsers require to have the `secure` attribute on cookies with `Samesite=none`
- CSRF_COOKIE_SECURE = True
- SESSION_COOKIE_SECURE = True
+ CSRF_COOKIE_SECURE = values.BooleanValue(True, environ_name="CSRF_COOKIE_SECURE")
+ SESSION_COOKIE_SECURE = values.BooleanValue(True, environ_name="SESSION_COOKIE_SECURE")
# Privacy
SECURE_REFERRER_POLICY = "same-origin"
@@ -0,0 +1,40 @@
{
lib,
buildPythonPackage,
fetchFromGitHub,
setuptools,
django,
}:
buildPythonPackage (finalAttrs: {
pname = "django-ltree";
version = "0.6.0";
pyproject = true;
src = fetchFromGitHub {
owner = "mariocesar";
repo = "django-ltree";
tag = "v${finalAttrs.version}";
hash = "sha256-XN2znH9bNU8jaY2HC8qmSR6VqShcEFGAtNtb/5aLgic=";
};
build-system = [
setuptools
];
dependencies = [
django
];
pythonImportsCheck = [
"django_ltree"
];
meta = {
description = "An ltree extension implementation to support hierarchical tree-like data using the native Postgres extension ltree in django models";
homepage = "https://github.com/mariocesar/django-ltree";
changelog = "https://github.com/mariocesar/django-ltree/releases/tag/${finalAttrs.src.tag}";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ soyouzpanda ];
};
})
@@ -0,0 +1,73 @@
{
lib,
buildPythonPackage,
fetchFromGitHub,
dj-database-url,
django,
django-test-migrations,
djangorestframework,
packaging,
pytest-cov-stub,
pytest-django,
pytest-dotenv,
pytestCheckHook,
setuptools,
setuptools-scm,
}:
buildPythonPackage (finalAttrs: {
pname = "djangorestframework-api-key";
version = "3.1.0";
pyproject = true;
src = fetchFromGitHub {
owner = "florimondmanca";
repo = "djangorestframework-api-key";
tag = "v${finalAttrs.version}";
hash = "sha256-TyYSO3OQslipl2T5BtsTABaTJD4HMCX61TOZXNR+lXE=";
};
# Use python-dotenv instead of django-dotenv
# as django-dotenv has not been maintained for
# years.
patchPhase = ''
runHook prePatch
substituteInPlace tests/conftest.py test_project/manage.py \
--replace-fail 'read_dotenv' 'load_dotenv'
runHook postPatch
'';
build-system = [
setuptools
setuptools-scm
];
dependencies = [
django
djangorestframework
packaging
];
nativeCheckInputs = [
dj-database-url
django-test-migrations
pytest-cov-stub
pytest-django
pytest-dotenv
pytestCheckHook
];
pythonImportsCheck = [
"rest_framework_api_key"
];
meta = {
description = "API key permissions for Django REST Framework";
homepage = "https://github.com/florimondmanca/djangorestframework-api-key";
changelog = "https://github.com/florimondmanca/djangorestframework-api-key/blob/${finalAttrs.src.tag}/CHANGELOG.md";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ soyouzpanda ];
};
})
+6
View File
@@ -4282,6 +4282,8 @@ self: super: with self; {
callPackage ../development/python-modules/django-login-required-middleware
{ };
django-ltree = callPackage ../development/python-modules/django-ltree { };
django-mailbox = callPackage ../development/python-modules/django-mailbox { };
django-mailman3 = callPackage ../development/python-modules/django-mailman3 { };
@@ -4507,6 +4509,10 @@ self: super: with self; {
djangorestframework = callPackage ../development/python-modules/djangorestframework { };
djangorestframework-api-key =
callPackage ../development/python-modules/djangorestframework-api-key
{ };
djangorestframework-camel-case =
callPackage ../development/python-modules/djangorestframework-camel-case
{ };