aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.github/actions/reusable-deploy/action.yml46
-rw-r--r--.github/actions/reusable-prepare-peertube-build/action.yml31
-rw-r--r--.github/actions/reusable-prepare-peertube-run/action.yml16
-rw-r--r--.github/workflows/benchmark.yml69
-rw-r--r--.github/workflows/codeql.yml68
-rw-r--r--.github/workflows/codeql/codeql-config.yml4
-rw-r--r--.github/workflows/docker.yml70
-rw-r--r--.github/workflows/nightly.yml33
-rw-r--r--.github/workflows/stats.yml50
-rw-r--r--.github/workflows/test.yml27
-rw-r--r--.gitlab-ci.yml84
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html128
-rw-r--r--client/src/app/+about/about.component.html6
-rw-r--r--client/src/app/+login/login.component.html3
-rw-r--r--client/src/app/menu/menu.component.html5
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts5
-rw-r--r--config/test.yaml1
-rw-r--r--package.json1
-rw-r--r--server/controllers/api/plugins.ts7
-rw-r--r--server/controllers/api/videos/upload.ts9
-rw-r--r--server/helpers/decache.ts78
-rw-r--r--server/initializers/config.ts4
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/client-html.ts4
-rw-r--r--server/lib/plugins/plugin-manager.ts4
-rw-r--r--server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts35
-rw-r--r--server/lib/uploadx.ts10
-rw-r--r--server/middlewares/validators/plugins.ts6
-rw-r--r--server/tests/cli/peertube.ts19
-rw-r--r--server/tools/peertube-plugins.ts3
-rw-r--r--shared/extra-utils/server/plugins-command.ts5
-rw-r--r--shared/models/plugins/client/plugin-selector-id.type.ts11
-rw-r--r--shared/models/plugins/server/api/install-plugin.model.ts1
-rw-r--r--support/doc/development/ci.md40
-rw-r--r--yarn.lock12
35 files changed, 561 insertions, 336 deletions
diff --git a/.github/actions/reusable-deploy/action.yml b/.github/actions/reusable-deploy/action.yml
new file mode 100644
index 000000000..bc69a2e43
--- /dev/null
+++ b/.github/actions/reusable-deploy/action.yml
@@ -0,0 +1,46 @@
1name: "Reusable deploy on builds.joinpeertube.org"
2
3description: "Reusable deploy on builds.joinpeertube.org"
4
5inputs:
6 source:
7 required: true
8 description: "Source file/files/directory/directories to deploy"
9 destination:
10 required: true
11 description: "Destination directory on builds.joinpeertube.org"
12 knownHosts:
13 required: true
14 description: "Known hosts"
15 deployKey:
16 required: true
17 description: "Deploy key"
18 deployUser:
19 required: true
20 description: "Deploy user"
21 deployHost:
22 required: true
23 description: "Deploy host"
24
25
26runs:
27 using: "composite"
28
29 steps:
30 - name: "Deploy"
31 shell: bash
32 run: |
33 mkdir -p ~/.ssh
34 chmod 700 ~/.ssh
35
36 echo "Adding ssh key to known hosts"
37 echo -e "${{ inputs.knownHosts }}" > ~/.ssh/known_hosts;
38
39 eval `ssh-agent -s`
40
41 echo "Adding ssh deploy key"
42 ssh-add <(echo "${{ inputs.deployKey }}");
43
44 echo "Uploading files"
45
46 scp ${{ inputs.source }} ${{ inputs.deployUser }}@${{ inputs.deployHost }}:../../web/${{ inputs.destination }};
diff --git a/.github/actions/reusable-prepare-peertube-build/action.yml b/.github/actions/reusable-prepare-peertube-build/action.yml
new file mode 100644
index 000000000..41ebf71c5
--- /dev/null
+++ b/.github/actions/reusable-prepare-peertube-build/action.yml
@@ -0,0 +1,31 @@
1name: "Reusable prepare PeerTube build"
2
3description: "Reusable prepare PeerTube build"
4
5inputs:
6 node-version:
7 required: true
8 description: 'NodeJS version'
9
10runs:
11 using: "composite"
12
13 steps:
14 - name: Use Node.js
15 uses: actions/setup-node@v1
16 with:
17 node-version: ${{ inputs.node-version }}
18
19 - name: Cache Node.js modules
20 uses: actions/cache@v2
21 with:
22 path: |
23 **/node_modules
24 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
25 restore-keys: |
26 ${{ runner.OS }}-node-
27 ${{ runner.OS }}-
28
29 - name: Install dependencies
30 shell: bash
31 run: yarn install --frozen-lockfile
diff --git a/.github/actions/reusable-prepare-peertube-run/action.yml b/.github/actions/reusable-prepare-peertube-run/action.yml
new file mode 100644
index 000000000..1a6cd2cfd
--- /dev/null
+++ b/.github/actions/reusable-prepare-peertube-run/action.yml
@@ -0,0 +1,16 @@
1name: "Reusable prepare PeerTube run"
2description: "Reusable prepare PeerTube run"
3
4runs:
5 using: "composite"
6
7 steps:
8 - name: Setup system dependencies
9 shell: bash
10 run: |
11 sudo apt-get install postgresql-client-common redis-tools parallel
12 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
13 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
14 mkdir -p $HOME/bin
15 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
16 echo "$HOME/bin" >> $GITHUB_PATH
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 86f675432..7e8259d27 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -29,48 +29,15 @@ jobs:
29 env: 29 env:
30 PGUSER: peertube 30 PGUSER: peertube
31 PGHOST: localhost 31 PGHOST: localhost
32 NODE_PENDING_JOB_WAIT: 500
33 32
34 steps: 33 steps:
35 - uses: actions/checkout@v2 34 - uses: actions/checkout@v2
36 35
37 - name: Use Node.js 36 - uses: './.github/actions/reusable-prepare-peertube-build'
38 uses: actions/setup-node@v1
39 with: 37 with:
40 node-version: '12.x' 38 node-version: '12.x'
41 39
42 - name: Setup system dependencies 40 - uses: './.github/actions/reusable-prepare-peertube-run'
43 run: |
44 sudo apt-get install postgresql-client-common redis-tools parallel
45 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
46 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
47 mkdir -p $HOME/bin
48 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
49 echo "$HOME/bin" >> $GITHUB_PATH
50
51 - name: Cache Node.js modules
52 uses: actions/cache@v2
53 with:
54 path: |
55 **/node_modules
56 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
57 restore-keys: |
58 ${{ runner.OS }}-node-
59 ${{ runner.OS }}-
60
61 - name: Cache fixtures
62 uses: actions/cache@v2
63 with:
64 path: |
65 fixtures
66 key: ${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-${{ hashFiles('fixtures/*') }}
67 restore-keys: |
68 ${{ runner.OS }}-fixtures-${{ matrix.test_suite }}-
69 ${{ runner.OS }}-fixtures-
70 ${{ runner.OS }}-
71
72 - name: Install dependencies
73 run: yarn install --frozen-lockfile
74 41
75 - name: Build 42 - name: Build
76 run: | 43 run: |
@@ -111,27 +78,11 @@ jobs:
111 cat benchmark.json build-time.json startup-time.json 78 cat benchmark.json build-time.json startup-time.json
112 79
113 - name: Upload benchmark result 80 - name: Upload benchmark result
114 env: 81 uses: './.github/actions/reusable-deploy'
115 STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }} 82 with:
116 STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }} 83 source: benchmark.json build-time.json startup-time.json
117 STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }} 84 destination: peertube-stats
118 STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }} 85 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
119 run: | 86 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
120 mkdir -p ~/.ssh 87 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
121 chmod 700 ~/.ssh 88 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
122 if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
123 echo "Adding ssh key to known hosts"
124 echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
125 fi
126
127 eval `ssh-agent -s`
128
129 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
130 echo "Adding ssh reployement key"
131 ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
132 fi
133
134 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
135 echo "Uploading files"
136 scp benchmark.json build-time.json startup-time.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
137 fi
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000..8764cdd0e
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,68 @@
1# For most projects, this workflow file will not need changing; you simply need
2# to commit it to your repository.
3#
4# You may wish to alter this file to override the set of languages analyzed,
5# or to provide custom queries or build logic.
6#
7# ******** NOTE ********
8# We have attempted to detect the languages in your repository. Please check
9# the `language` matrix defined below to confirm you have the correct set of
10# supported CodeQL languages.
11#
12name: "CodeQL"
13
14on:
15 push:
16 branches: [ develop, next ]
17 schedule:
18 - cron: '36 9 * * 5'
19
20jobs:
21 analyze:
22 name: Analyze
23 runs-on: ubuntu-latest
24 permissions:
25 actions: read
26 contents: read
27 security-events: write
28
29 strategy:
30 fail-fast: false
31 matrix:
32 language: [ 'javascript' ]
33 # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
34 # Learn more about CodeQL language support at https://git.io/codeql-language-support
35
36 steps:
37 - name: Checkout repository
38 uses: actions/checkout@v2
39
40 # Initializes the CodeQL tools for scanning.
41 - name: Initialize CodeQL
42 uses: github/codeql-action/init@v1
43 with:
44 languages: ${{ matrix.language }}
45 config-file: ./.github/workflows/codeql/codeql-config.yml
46 # If you wish to specify custom queries, you can do so here or in a config file.
47 # By default, queries listed here will override any specified in a config file.
48 # Prefix the list here with "+" to use these queries and those in the config file.
49 # queries: ./path/to/local/query, your-org/your-repo/queries@main
50
51 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
52 # If this step fails, then you should remove it and run the build manually (see below)
53 - name: Autobuild
54 uses: github/codeql-action/autobuild@v1
55
56 # ℹ️ Command-line programs to run using the OS shell.
57 # 📚 https://git.io/JvXDl
58
59 # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
60 # and modify them (or add more) to build your code if your project
61 # uses a compiled language
62
63 #- run: |
64 # make bootstrap
65 # make release
66
67 - name: Perform CodeQL Analysis
68 uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/codeql/codeql-config.yml b/.github/workflows/codeql/codeql-config.yml
new file mode 100644
index 000000000..8b771ae99
--- /dev/null
+++ b/.github/workflows/codeql/codeql-config.yml
@@ -0,0 +1,4 @@
1name: "PeerTube CodeQL config"
2
3paths-ignore:
4 - server/tests
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 000000000..7afe641b3
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,70 @@
1name: Docker
2
3on:
4 push:
5 branches:
6 - 'master'
7 schedule:
8 - cron: '0 3 * * *'
9
10jobs:
11 generate-matrix:
12 name: Generate matrix for Docker build
13 runs-on: ubuntu-latest
14 outputs:
15 matrix: ${{ steps.set-matrix.outputs.matrix }}
16 steps:
17 - name: Checkout
18 uses: actions/checkout@v2
19 with:
20 ref: master
21 - name: Set matrix for build
22 id: set-matrix
23 run: |
24 # FIXME: https://github.com/actions/checkout/issues/290
25 git fetch --force --tags
26
27 one="{ \"file\": \"./support/docker/production/Dockerfile.bullseye\", \"ref\": \"develop\", \"tags\": \"chocobozzz/peertube:develop-bullseye\" }"
28 two="{ \"file\": \"./support/docker/production/Dockerfile.buster\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube:production-buster,chocobozzz/peertube:$(git describe --abbrev=0)-buster\" }"
29 three="{ \"file\": \"./support/docker/production/Dockerfile.nginx\", \"ref\": \"master\", \"tags\": \"chocobozzz/peertube-webserver:latest\" }"
30
31 matrix="[$one,$two,$three]"
32 echo ::set-output name=matrix::{\"include\":$(echo $matrix)}
33
34 docker:
35 runs-on: ubuntu-latest
36
37 needs: generate-matrix
38
39 strategy:
40 matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
41 fail-fast: false
42
43 steps:
44 -
45 name: Set up QEMU
46 uses: docker/setup-qemu-action@v1
47 -
48 name: Set up Docker Buildx
49 uses: docker/setup-buildx-action@v1
50 -
51 name: Login to DockerHub
52 uses: docker/login-action@v1
53 with:
54 username: ${{ secrets.DOCKERHUB_USERNAME }}
55 password: ${{ secrets.DOCKERHUB_TOKEN }}
56
57 -
58 name: Checkout develop
59 uses: actions/checkout@v2
60 with:
61 ref: ${{ matrix.ref }}
62 -
63 name: Docker build
64 uses: docker/build-push-action@v2
65 with:
66 context: '.'
67 platforms: linux/amd64,linux/arm64
68 push: true
69 file: ${{ matrix.file }}
70 tags: ${{ matrix.tags }}
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 000000000..23898b7ef
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,33 @@
1name: Nightly
2
3on:
4 schedule:
5 - cron: '0 3 * * *'
6
7jobs:
8
9 nightly:
10 runs-on: ubuntu-latest
11
12 steps:
13 -
14 name: Checkout develop
15 uses: actions/checkout@v2
16 with:
17 ref: develop
18
19 - uses: './.github/actions/reusable-prepare-peertube-build'
20 with:
21 node-version: '14.x'
22
23 - name: Build
24 run: npm run nightly
25
26 - uses: './.github/actions/reusable-deploy'
27 with:
28 source: ./peertube-nightly-*
29 destination: nightly
30 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
31 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
32 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
33 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
index e211f6a3b..c87e6fb77 100644
--- a/.github/workflows/stats.yml
+++ b/.github/workflows/stats.yml
@@ -1,4 +1,4 @@
1name: "Stats" 1name: Stats
2 2
3on: 3on:
4 push: 4 push:
@@ -20,24 +20,10 @@ jobs:
20 steps: 20 steps:
21 - uses: actions/checkout@v2 21 - uses: actions/checkout@v2
22 22
23 - name: Use Node.js 23 - uses: './.github/actions/reusable-prepare-peertube-build'
24 uses: actions/setup-node@v1
25 with: 24 with:
26 node-version: '14.x' 25 node-version: '14.x'
27 26
28 - name: Cache Node.js modules
29 uses: actions/cache@v2
30 with:
31 path: |
32 **/node_modules
33 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
34 restore-keys: |
35 ${{ runner.OS }}-node-
36 ${{ runner.OS }}-
37
38 - name: Install dependencies
39 run: yarn install --frozen-lockfile
40
41 - name: Angular bundlewatch 27 - name: Angular bundlewatch
42 uses: jackyef/bundlewatch-gh-action@master 28 uses: jackyef/bundlewatch-gh-action@master
43 with: 29 with:
@@ -73,27 +59,11 @@ jobs:
73 59
74 - name: Upload stats 60 - name: Upload stats
75 if: github.event_name != 'pull_request' 61 if: github.event_name != 'pull_request'
76 env: 62 uses: './.github/actions/reusable-deploy'
77 STATS_DEPLOYEMENT_KNOWN_HOSTS: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }} 63 with:
78 STATS_DEPLOYEMENT_KEY: ${{ secrets.STATS_DEPLOYEMENT_KEY }} 64 source: lighthouse.json client-build-stats.json scc.json
79 STATS_DEPLOYEMENT_USER: ${{ secrets.STATS_DEPLOYEMENT_USER }} 65 destination: peertube-stats
80 STATS_DEPLOYEMENT_HOST: ${{ secrets.STATS_DEPLOYEMENT_HOST }} 66 knownHosts: ${{ secrets.STATS_DEPLOYEMENT_KNOWN_HOSTS }}
81 run: | 67 deployKey: ${{ secrets.STATS_DEPLOYEMENT_KEY }}
82 mkdir -p ~/.ssh 68 deployUser: ${{ secrets.STATS_DEPLOYEMENT_USER }}
83 chmod 700 ~/.ssh 69 deployHost: ${{ secrets.STATS_DEPLOYEMENT_HOST }}
84 if [ ! -z ${STATS_DEPLOYEMENT_KNOWN_HOSTS+x} ]; then
85 echo "Adding ssh key to known hosts"
86 echo -e "${STATS_DEPLOYEMENT_KNOWN_HOSTS}" > ~/.ssh/known_hosts;
87 fi
88
89 eval `ssh-agent -s`
90
91 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
92 echo "Adding ssh reployement key"
93 ssh-add <(echo "${STATS_DEPLOYEMENT_KEY}");
94 fi
95
96 if [ ! -z ${STATS_DEPLOYEMENT_KEY+x} ]; then
97 echo "Uploading files"
98 scp lighthouse.json client-build-stats.json scc.json ${STATS_DEPLOYEMENT_USER}@${STATS_DEPLOYEMENT_HOST}:../../web/peertube-stats;
99 fi
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 78a9a28c0..030ec3790 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
1name: Test Suite 1name: Test
2 2
3on: 3on:
4 push: 4 push:
@@ -50,29 +50,11 @@ jobs:
50 steps: 50 steps:
51 - uses: actions/checkout@v2 51 - uses: actions/checkout@v2
52 52
53 - name: Use Node.js 53 - uses: './.github/actions/reusable-prepare-peertube-build'
54 uses: actions/setup-node@v1
55 with: 54 with:
56 node-version: '12.x' 55 node-version: '12.x'
57 56
58 - name: Setup system dependencies 57 - uses: './.github/actions/reusable-prepare-peertube-run'
59 run: |
60 sudo apt-get install postgresql-client-common redis-tools parallel
61 wget --quiet --no-check-certificate "https://download.cpy.re/ffmpeg/ffmpeg-release-4.3.1-64bit-static.tar.xz"
62 tar xf ffmpeg-release-4.3.1-64bit-static.tar.xz
63 mkdir -p $HOME/bin
64 cp ffmpeg-*/{ffmpeg,ffprobe} $HOME/bin
65 echo "$HOME/bin" >> $GITHUB_PATH
66
67 - name: Cache Node.js modules
68 uses: actions/cache@v2
69 with:
70 path: |
71 **/node_modules
72 key: ${{ runner.OS }}-node-${{ hashFiles('**/yarn.lock') }}
73 restore-keys: |
74 ${{ runner.OS }}-node-
75 ${{ runner.OS }}-
76 58
77 - name: Cache fixtures 59 - name: Cache fixtures
78 uses: actions/cache@v2 60 uses: actions/cache@v2
@@ -85,9 +67,6 @@ jobs:
85 ${{ runner.OS }}-fixtures- 67 ${{ runner.OS }}-fixtures-
86 ${{ runner.OS }}- 68 ${{ runner.OS }}-
87 69
88 - name: Install dependencies
89 run: yarn install --frozen-lockfile
90
91 - name: Set env test variable (schedule) 70 - name: Set env test variable (schedule)
92 if: github.event_name != 'schedule' 71 if: github.event_name != 'schedule'
93 run: | 72 run: |
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index ad94c8cab..000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,84 +0,0 @@
1image: chocobozzz/peertube-ci:14
2
3stages:
4 - clients
5 - docker-nightly
6
7cache:
8 key: yarn
9 paths:
10 - .yarn-cache
11 - cached-fixtures
12
13# build-openapi-clients:
14# stage: clients
15# only:
16# refs:
17# - master
18# - schedules
19# changes:
20# - support/doc/api/openapi.yaml
21# script:
22# - apt-get update -qq
23# - apt-get -yqqq install openjdk-8-jre
24# - yarn install --pure-lockfile
25# - scripts/openapi-peertube-version.sh
26# - scripts/openapi-clients.sh
27
28build-nightly:
29 stage: docker-nightly
30 only:
31 - schedules
32 script:
33 - yarn install --pure-lockfile --cache-folder .yarn-cache
34 - npm run nightly
35 - mkdir "${HOME}/.ssh"
36 - chmod 700 "${HOME}/.ssh"
37 - if [ ! -z ${DEPLOYEMENT_KNOWN_HOSTS+x} ]; then echo -e "${DEPLOYEMENT_KNOWN_HOSTS}" > ${HOME}/.ssh/known_hosts; fi
38 - eval `ssh-agent -s`
39 - if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then ssh-add <(echo "${DEPLOYEMENT_KEY}"); fi
40 - if [ ! -z ${DEPLOYEMENT_KEY+x} ]; then scp ./peertube-nightly-* ${DEPLOYEMENT_USER}@${DEPLOYEMENT_HOST}:../../web/nightly; fi
41
42.docker: &docker
43 stage: docker-nightly
44 cache: {}
45 image:
46 name: gcr.io/kaniko-project/executor:debug
47 entrypoint: [""]
48 before_script:
49 - mkdir -p /kaniko/.docker
50 - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json
51 script:
52 - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $DOCKERFILE --destination $DOCKER_IMAGE_NAME
53
54build-docker-develop:
55 <<: *docker
56 only:
57 - schedules
58 variables:
59 DOCKER_IMAGE_NAME: chocobozzz/peertube:develop-bullseye
60 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
61
62build-docker-webserver:
63 <<: *docker
64 only:
65 - schedules
66 variables:
67 DOCKER_IMAGE_NAME: chocobozzz/peertube-webserver
68 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.nginx
69
70build-docker-tag:
71 <<: *docker
72 only:
73 - tags
74 variables:
75 DOCKER_IMAGE_NAME: chocobozzz/peertube:$CI_COMMIT_TAG-bullseye
76 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
77
78build-docker-master:
79 <<: *docker
80 only:
81 - master
82 variables:
83 DOCKER_IMAGE_NAME: chocobozzz/peertube:production-bullseye
84 DOCKERFILE: $CI_PROJECT_DIR/support/docker/production/Dockerfile.bullseye
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 1026c4e0d..7f2a6aa77 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -116,95 +116,99 @@
116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> 116 <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container>
117 </div> 117 </div>
118 118
119 <div class="anchor" id="moderation"></div> 119 <div myPluginSelector pluginSelectorId="about-instance-moderation">
120 <a 120 <div class="anchor" id="moderation"></div>
121 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
122 class="anchor-link"
123 routerLink="/about/instance"
124 fragment="moderation"
125 #anchorLink
126 (click)="onClickCopyLink(anchorLink)">
127 <h2 i18n class="middle-title">
128 MODERATION
129 </h2>
130 </a>
131
132 <div class="block moderation-information" *ngIf="html.moderationInformation">
133 <div class="anchor" id="moderation-information"></div>
134 <a 121 <a
122 *ngIf="html.moderationInformation || html.codeOfConduct || html.terms"
135 class="anchor-link" 123 class="anchor-link"
136 routerLink="/about/instance" 124 routerLink="/about/instance"
137 fragment="moderation-information" 125 fragment="moderation"
138 #anchorLink 126 #anchorLink
139 (click)="onClickCopyLink(anchorLink)"> 127 (click)="onClickCopyLink(anchorLink)">
140 <h3 i18n class="section-title">Moderation information</h3> 128 <h2 i18n class="middle-title">
129 MODERATION
130 </h2>
141 </a> 131 </a>
142 132
143 <div [innerHTML]="html.moderationInformation"></div> 133 <div class="block moderation-information" *ngIf="html.moderationInformation">
144 </div> 134 <div class="anchor" id="moderation-information"></div>
135 <a
136 class="anchor-link"
137 routerLink="/about/instance"
138 fragment="moderation-information"
139 #anchorLink
140 (click)="onClickCopyLink(anchorLink)">
141 <h3 i18n class="section-title">Moderation information</h3>
142 </a>
145 143
146 <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> 144 <div [innerHTML]="html.moderationInformation"></div>
147 <div class="anchor" id="code-of-conduct"></div> 145 </div>
148 <a
149 class="anchor-link"
150 routerLink="/about/instance"
151 fragment="code-of-conduct"
152 #anchorLink
153 (click)="onClickCopyLink(anchorLink)">
154 <h3 i18n class="section-title">Code of conduct</h3>
155 </a>
156 146
157 <div [innerHTML]="html.codeOfConduct"></div> 147 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
158 </div> 148 <div class="anchor" id="code-of-conduct"></div>
149 <a
150 class="anchor-link"
151 routerLink="/about/instance"
152 fragment="code-of-conduct"
153 #anchorLink
154 (click)="onClickCopyLink(anchorLink)">
155 <h3 i18n class="section-title">Code of conduct</h3>
156 </a>
159 157
160 <div class="block terms"> 158 <div [innerHTML]="html.codeOfConduct"></div>
161 <div class="anchor" id="terms"></div> 159 </div>
162 <a
163 class="anchor-link"
164 routerLink="/about/instance"
165 fragment="terms"
166 #anchorLink
167 (click)="onClickCopyLink(anchorLink)">
168 <h3 i18n class="section-title">Terms</h3>
169 </a>
170 160
171 <div [innerHTML]="html.terms"></div> 161 <div class="block terms">
172 </div> 162 <div class="anchor" id="terms"></div>
163 <a
164 class="anchor-link"
165 routerLink="/about/instance"
166 fragment="terms"
167 #anchorLink
168 (click)="onClickCopyLink(anchorLink)">
169 <h3 i18n class="section-title">Terms</h3>
170 </a>
173 171
174 <div class="anchor" id="other-information"></div> 172 <div [innerHTML]="html.terms"></div>
175 <a 173 </div>
176 *ngIf="html.hardwareInformation" 174 </div>
177 class="anchor-link"
178 routerLink="/about/instance"
179 fragment="other-information"
180 #anchorLink
181 (click)="onClickCopyLink(anchorLink)">
182 <h2 i18n class="middle-title">
183 OTHER INFORMATION
184 </h2>
185 </a>
186 175
187 <div class="block hardware-information" *ngIf="html.hardwareInformation"> 176 <div myPluginSelector pluginSelectorId="about-instance-other-information">
188 <div class="anchor" id="hardware-information"></div> 177 <div class="anchor" id="other-information"></div>
189 <a 178 <a
179 *ngIf="html.hardwareInformation"
190 class="anchor-link" 180 class="anchor-link"
191 routerLink="/about/instance" 181 routerLink="/about/instance"
192 fragment="hardware-information" 182 fragment="other-information"
193 #anchorLink 183 #anchorLink
194 (click)="onClickCopyLink(anchorLink)"> 184 (click)="onClickCopyLink(anchorLink)">
195 <h3 i18n class="section-title">Hardware information</h3> 185 <h2 i18n class="middle-title">
186 OTHER INFORMATION
187 </h2>
196 </a> 188 </a>
197 189
198 <div [innerHTML]="html.hardwareInformation"></div> 190 <div class="block hardware-information" *ngIf="html.hardwareInformation">
191 <div class="anchor" id="hardware-information"></div>
192 <a
193 class="anchor-link"
194 routerLink="/about/instance"
195 fragment="hardware-information"
196 #anchorLink
197 (click)="onClickCopyLink(anchorLink)">
198 <h3 i18n class="section-title">Hardware information</h3>
199 </a>
200
201 <div [innerHTML]="html.hardwareInformation"></div>
202 </div>
199 </div> 203 </div>
200 </div> 204 </div>
201 205
202 <div class="col-md-12 col-xl-6"> 206 <div class="col-md-12 col-xl-6" myPluginSelector pluginSelectorId="about-instance-features">
203 <h2 class="sr-only" i18n>FEATURES</h2> 207 <h2 class="sr-only" i18n>FEATURES</h2>
204 <my-instance-features-table></my-instance-features-table> 208 <my-instance-features-table></my-instance-features-table>
205 </div> 209 </div>
206 210
207 <div class="col"> 211 <div class="col" myPluginSelector pluginSelectorId="about-instance-statistics">
208 <div class="anchor" id="statistics"></div> 212 <div class="anchor" id="statistics"></div>
209 <a 213 <a
210 class="anchor-link" 214 class="anchor-link"
diff --git a/client/src/app/+about/about.component.html b/client/src/app/+about/about.component.html
index 1ab00c5df..63d429ebf 100644
--- a/client/src/app/+about/about.component.html
+++ b/client/src/app/+about/about.component.html
@@ -2,11 +2,11 @@
2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }"> 2 <div class="sub-menu" [ngClass]="{ 'sub-menu-fixed': !isBroadcastMessageDisplayed }">
3 3
4 <div class="links"> 4 <div class="links">
5 <a i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a> 5 <a myPluginSelector pluginSelectorId="about-menu-instance" i18n routerLink="instance" routerLinkActive="active" class="title-page title-page-about">Instance</a>
6 6
7 <a i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a> 7 <a myPluginSelector pluginSelectorId="about-menu-peertube" i18n routerLink="peertube" routerLinkActive="active" class="title-page title-page-about">PeerTube</a>
8 8
9 <a i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a> 9 <a myPluginSelector pluginSelectorId="about-menu-network" i18n routerLink="follows" routerLinkActive="active" class="title-page title-page-about">Network</a>
10 </div> 10 </div>
11 </div> 11 </div>
12 12
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 90eea1505..531b06dc9 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -48,7 +48,8 @@
48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid"> 48 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
49 49
50 <div class="additionnal-links"> 50 <div class="additionnal-links">
51 <a i18n class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 51 <a i18n role="button" class="forgot-password-button" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
52
52 <div *ngIf="signupAllowed" class="signup-link"> 53 <div *ngIf="signupAllowed" class="signup-link">
53 <span>·</span> 54 <span>·</span>
54 <a i18n routerLink="/signup" class="create-an-account">Create an account</a> 55 <a i18n routerLink="/signup" class="create-an-account">Create an account</a>
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 46dd807ec..55fc27b5f 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -30,7 +30,10 @@
30 30
31 <div class="dropdown-divider"></div> 31 <div class="dropdown-divider"></div>
32 32
33 <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"> 33 <a
34 myPluginSelector pluginSelectorId="menu-user-dropdown-language-item"
35 ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()"
36 >
34 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon> 37 <my-global-icon iconName="language" aria-hidden="true"></my-global-icon>
35 <span i18n>Interface:</span> 38 <span i18n>Interface:</span>
36 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span> 39 <span class="ml-auto text-muted">{{ currentInterfaceLanguage }}</span>
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
index 71c31696a..2aa691f05 100644
--- a/client/src/assets/player/p2p-media-loader/hls-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -146,7 +146,9 @@ class Html5Hlsjs {
146 } 146 }
147 147
148 duration () { 148 duration () {
149 return this._duration || this.videoElement.duration || 0 149 if (!isNaN(this.videoElement.duration)) return this.videoElement.duration
150
151 return this._duration || 0
150 } 152 }
151 153
152 seekable () { 154 seekable () {
@@ -366,6 +368,7 @@ class Html5Hlsjs {
366 368
367 this.isLive = data.details.live 369 this.isLive = data.details.live
368 this.dvrDuration = data.details.totalduration 370 this.dvrDuration = data.details.totalduration
371
369 this._duration = this.isLive ? Infinity : data.details.totalduration 372 this._duration = this.isLive ? Infinity : data.details.totalduration
370 }) 373 })
371 374
diff --git a/config/test.yaml b/config/test.yaml
index 2e7f982d3..461e1b4ba 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -155,6 +155,7 @@ search:
155federation: 155federation:
156 videos: 156 videos:
157 federate_unlisted: true 157 federate_unlisted: true
158 cleanup_remote_interactions: false
158 159
159views: 160views:
160 videos: 161 videos:
diff --git a/package.json b/package.json
index 6cc00d1ae..b03f5dcdb 100644
--- a/package.json
+++ b/package.json
@@ -90,7 +90,6 @@
90 "cookie-parser": "^1.4.3", 90 "cookie-parser": "^1.4.3",
91 "cors": "^2.8.1", 91 "cors": "^2.8.1",
92 "create-torrent": "^5.0.0", 92 "create-torrent": "^5.0.0",
93 "decache": "^4.6.0",
94 "deep-object-diff": "^1.1.0", 93 "deep-object-diff": "^1.1.0",
95 "email-templates": "^8.0.3", 94 "email-templates": "^8.0.3",
96 "execa": "^5.1.1", 95 "execa": "^5.1.1",
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 2de7fe41f..de9e055dc 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -144,8 +144,13 @@ async function installPlugin (req: express.Request, res: express.Response) {
144 144
145 const fromDisk = !!body.path 145 const fromDisk = !!body.path
146 const toInstall = body.npmName || body.path 146 const toInstall = body.npmName || body.path
147
148 const pluginVersion = body.pluginVersion && body.npmName
149 ? body.pluginVersion
150 : undefined
151
147 try { 152 try {
148 const plugin = await PluginManager.Instance.install(toInstall, undefined, fromDisk) 153 const plugin = await PluginManager.Instance.install(toInstall, pluginVersion, fromDisk)
149 154
150 return res.json(plugin.toFormattedJSON()) 155 return res.json(plugin.toFormattedJSON())
151 } catch (err) { 156 } catch (err) {
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts
index 6773b500f..c827f6bf0 100644
--- a/server/controllers/api/videos/upload.ts
+++ b/server/controllers/api/videos/upload.ts
@@ -8,6 +8,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url' 8import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
9import { generateWebTorrentVideoFilename } from '@server/lib/paths' 9import { generateWebTorrentVideoFilename } from '@server/lib/paths'
10import { Redis } from '@server/lib/redis' 10import { Redis } from '@server/lib/redis'
11import { uploadx } from '@server/lib/uploadx'
11import { 12import {
12 addMoveToObjectStorageJob, 13 addMoveToObjectStorageJob,
13 addOptimizeOrMergeAudioJob, 14 addOptimizeOrMergeAudioJob,
@@ -19,7 +20,6 @@ import { VideoPathManager } from '@server/lib/video-path-manager'
19import { buildNextVideoState } from '@server/lib/video-state' 20import { buildNextVideoState } from '@server/lib/video-state'
20import { openapiOperationDoc } from '@server/middlewares/doc' 21import { openapiOperationDoc } from '@server/middlewares/doc'
21import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 22import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
22import { Uploadx } from '@uploadx/core'
23import { VideoCreate, VideoState } from '../../../../shared' 23import { VideoCreate, VideoState } from '../../../../shared'
24import { HttpStatusCode } from '../../../../shared/models' 24import { HttpStatusCode } from '../../../../shared/models'
25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 25import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
@@ -41,8 +41,8 @@ import {
41 authenticate, 41 authenticate,
42 videosAddLegacyValidator, 42 videosAddLegacyValidator,
43 videosAddResumableInitValidator, 43 videosAddResumableInitValidator,
44 videosResumableUploadIdValidator, 44 videosAddResumableValidator,
45 videosAddResumableValidator 45 videosResumableUploadIdValidator
46} from '../../../middlewares' 46} from '../../../middlewares'
47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 47import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
48import { VideoModel } from '../../../models/video/video' 48import { VideoModel } from '../../../models/video/video'
@@ -52,9 +52,6 @@ const lTags = loggerTagsFactory('api', 'video')
52const auditLogger = auditLoggerFactory('videos') 52const auditLogger = auditLoggerFactory('videos')
53const uploadRouter = express.Router() 53const uploadRouter = express.Router()
54 54
55const uploadx = new Uploadx({ directory: getResumableUploadPath() })
56uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
57
58const reqVideoFileAdd = createReqFiles( 55const reqVideoFileAdd = createReqFiles(
59 [ 'videofile', 'thumbnailfile', 'previewfile' ], 56 [ 'videofile', 'thumbnailfile', 'previewfile' ],
60 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT), 57 Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
diff --git a/server/helpers/decache.ts b/server/helpers/decache.ts
new file mode 100644
index 000000000..e31973b7a
--- /dev/null
+++ b/server/helpers/decache.ts
@@ -0,0 +1,78 @@
1// Thanks: https://github.com/dwyl/decache
2// We reuse this file to also uncache plugin base path
3
4import { extname } from 'path'
5
6function decachePlugin (pluginPath: string, libraryPath: string) {
7 const moduleName = find(libraryPath)
8
9 if (!moduleName) return
10
11 searchCache(moduleName, function (mod) {
12 delete require.cache[mod.id]
13 })
14
15 removeCachedPath(pluginPath)
16}
17
18function decacheModule (name: string) {
19 const moduleName = find(name)
20
21 if (!moduleName) return
22
23 searchCache(moduleName, function (mod) {
24 delete require.cache[mod.id]
25 })
26
27 removeCachedPath(moduleName)
28}
29
30// ---------------------------------------------------------------------------
31
32export {
33 decacheModule,
34 decachePlugin
35}
36
37// ---------------------------------------------------------------------------
38
39function find (moduleName: string) {
40 try {
41 return require.resolve(moduleName)
42 } catch {
43 return ''
44 }
45}
46
47function searchCache (moduleName: string, callback: (current: NodeModule) => void) {
48 const resolvedModule = require.resolve(moduleName)
49 let mod: NodeModule
50 const visited = {}
51
52 if (resolvedModule && ((mod = require.cache[resolvedModule]) !== undefined)) {
53 // Recursively go over the results
54 (function run (current) {
55 visited[current.id] = true
56
57 current.children.forEach(function (child) {
58 if (extname(child.filename) !== '.node' && !visited[child.id]) {
59 run(child)
60 }
61 })
62
63 // Call the specified callback providing the
64 // found module
65 callback(current)
66 })(mod)
67 }
68};
69
70function removeCachedPath (pluginPath: string) {
71 const pathCache = (module.constructor as any)._pathCache
72
73 Object.keys(pathCache).forEach(function (cacheKey) {
74 if (cacheKey.includes(pluginPath)) {
75 delete pathCache[cacheKey]
76 }
77 })
78}
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index dadda2a77..f3a7c6b6b 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -1,7 +1,7 @@
1import bytes from 'bytes' 1import bytes from 'bytes'
2import { IConfig } from 'config' 2import { IConfig } from 'config'
3import decache from 'decache'
4import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { decacheModule } from '@server/helpers/decache'
5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
6import { BroadcastMessageLevel } from '@shared/models/server' 6import { BroadcastMessageLevel } from '@shared/models/server'
7import { VideosRedundancyStrategy } from '../../shared/models' 7import { VideosRedundancyStrategy } from '../../shared/models'
@@ -497,7 +497,7 @@ export function reloadConfig () {
497 delete require.cache[fileName] 497 delete require.cache[fileName]
498 } 498 }
499 499
500 decache('config') 500 decacheModule('config')
501 } 501 }
502 502
503 purge() 503 purge()
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b8633e83e..c61c01d62 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -223,7 +223,7 @@ const SCHEDULER_INTERVALS_MS = {
223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day 223 REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day 224 REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute 225 UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 * 16 // 16 hours 226 REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60 // 1 hour
227} 227}
228 228
229// --------------------------------------------------------------------------- 229// ---------------------------------------------------------------------------
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 360b4667f..adc3d712e 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -350,10 +350,6 @@ class ClientHtml {
350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html') 350 return join(__dirname, '../../../client/dist/standalone/videos/embed.html')
351 } 351 }
352 352
353 private static addHtmlLang (htmlStringPage: string, paramLang: string) {
354 return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`)
355 }
356
357 private static addManifestContentHash (htmlStringPage: string) { 353 private static addManifestContentHash (htmlStringPage: string) {
358 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) 354 return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST)
359 } 355 }
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index d4d2a7edc..6c2f4764e 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -1,8 +1,8 @@
1import decache from 'decache'
2import express from 'express' 1import express from 'express'
3import { createReadStream, createWriteStream } from 'fs' 2import { createReadStream, createWriteStream } from 'fs'
4import { ensureDir, outputFile, readJSON } from 'fs-extra' 3import { ensureDir, outputFile, readJSON } from 'fs-extra'
5import { basename, join } from 'path' 4import { basename, join } from 'path'
5import { decachePlugin } from '@server/helpers/decache'
6import { MOAuthTokenUser, MUser } from '@server/types/models' 6import { MOAuthTokenUser, MUser } from '@server/types/models'
7import { getCompleteLocale } from '@shared/core-utils' 7import { getCompleteLocale } from '@shared/core-utils'
8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models' 8import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
@@ -420,7 +420,7 @@ export class PluginManager implements ServerHook {
420 420
421 // Delete cache if needed 421 // Delete cache if needed
422 const modulePath = join(pluginPath, packageJSON.library) 422 const modulePath = join(pluginPath, packageJSON.library)
423 decache(modulePath) 423 decachePlugin(pluginPath, modulePath)
424 const library: PluginLibrary = require(modulePath) 424 const library: PluginLibrary = require(modulePath)
425 425
426 if (!isLibraryCodeValid(library)) { 426 if (!isLibraryCodeValid(library)) {
diff --git a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
index d6e561cad..61e93eafa 100644
--- a/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
+++ b/server/lib/schedulers/remove-dangling-resumable-uploads-scheduler.ts
@@ -1,9 +1,7 @@
1import { map } from 'bluebird' 1
2import { readdir, remove, stat } from 'fs-extra'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 2import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { getResumableUploadPath } from '@server/helpers/upload'
5import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants' 3import { SCHEDULER_INTERVALS_MS } from '@server/initializers/constants'
6import { METAFILE_EXTNAME } from '@uploadx/core' 4import { uploadx } from '../uploadx'
7import { AbstractScheduler } from './abstract-scheduler' 5import { AbstractScheduler } from './abstract-scheduler'
8 6
9const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner') 7const lTags = loggerTagsFactory('scheduler', 'resumable-upload', 'cleaner')
@@ -22,36 +20,17 @@ export class RemoveDanglingResumableUploadsScheduler extends AbstractScheduler {
22 } 20 }
23 21
24 protected async internalExecute () { 22 protected async internalExecute () {
25 const path = getResumableUploadPath() 23 logger.debug('Removing dangling resumable uploads', lTags())
26 const files = await readdir(path)
27
28 const metafiles = files.filter(f => f.endsWith(METAFILE_EXTNAME))
29 24
30 if (metafiles.length === 0) return 25 const now = new Date().getTime()
31
32 logger.debug('Reading resumable video upload folder %s with %d files', path, metafiles.length, lTags())
33 26
34 try { 27 try {
35 await map(metafiles, metafile => { 28 // Remove files that were not updated since the last execution
36 return this.deleteIfOlderThan(metafile, this.lastExecutionTimeMs) 29 await uploadx.storage.purge(now - this.lastExecutionTimeMs)
37 }, { concurrency: 5 })
38 } catch (error) { 30 } catch (error) {
39 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() }) 31 logger.error('Failed to handle file during resumable video upload folder cleanup', { error, ...lTags() })
40 } finally { 32 } finally {
41 this.lastExecutionTimeMs = new Date().getTime() 33 this.lastExecutionTimeMs = now
42 }
43 }
44
45 private async deleteIfOlderThan (metafile: string, olderThan: number) {
46 const metafilePath = getResumableUploadPath(metafile)
47 const statResult = await stat(metafilePath)
48
49 // Delete uploads that started since a long time
50 if (statResult.ctimeMs < olderThan) {
51 await remove(metafilePath)
52
53 const datafile = metafilePath.replace(new RegExp(`${METAFILE_EXTNAME}$`), '')
54 await remove(datafile)
55 } 34 }
56 } 35 }
57 36
diff --git a/server/lib/uploadx.ts b/server/lib/uploadx.ts
new file mode 100644
index 000000000..11b1044db
--- /dev/null
+++ b/server/lib/uploadx.ts
@@ -0,0 +1,10 @@
1import express from 'express'
2import { getResumableUploadPath } from '@server/helpers/upload'
3import { Uploadx } from '@uploadx/core'
4
5const uploadx = new Uploadx({ directory: getResumableUploadPath() })
6uploadx.getUserId = (_, res: express.Response) => res.locals.oauth?.token.user.id
7
8export {
9 uploadx
10}
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 21171af23..c1e9ebefb 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -116,6 +116,9 @@ const installOrUpdatePluginValidator = [
116 body('npmName') 116 body('npmName')
117 .optional() 117 .optional()
118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), 118 .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'),
119 body('pluginVersion')
120 .optional()
121 .custom(isPluginVersionValid).withMessage('Should have a valid plugin version'),
119 body('path') 122 body('path')
120 .optional() 123 .optional()
121 .custom(isSafePath).withMessage('Should have a valid safe path'), 124 .custom(isSafePath).withMessage('Should have a valid safe path'),
@@ -129,6 +132,9 @@ const installOrUpdatePluginValidator = [
129 if (!body.path && !body.npmName) { 132 if (!body.path && !body.npmName) {
130 return res.fail({ message: 'Should have either a npmName or a path' }) 133 return res.fail({ message: 'Should have either a npmName or a path' })
131 } 134 }
135 if (body.pluginVersion && !body.npmName) {
136 return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
137 }
132 138
133 return next() 139 return next()
134 } 140 }
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index f2a984962..3ac440f84 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -207,6 +207,25 @@ describe('Test CLI wrapper', function () {
207 207
208 expect(res).to.not.contain('peertube-plugin-hello-world') 208 expect(res).to.not.contain('peertube-plugin-hello-world')
209 }) 209 })
210
211 it('Should install a plugin in requested version', async function () {
212 this.timeout(60000)
213
214 await cliCommand.execWithEnv(`${cmd} plugins install --npm-name peertube-plugin-hello-world --plugin-version 0.0.17`)
215 })
216
217 it('Should list installed plugins, in correct version', async function () {
218 const res = await cliCommand.execWithEnv(`${cmd} plugins list`)
219
220 expect(res).to.contain('peertube-plugin-hello-world')
221 expect(res).to.contain('0.0.17')
222 })
223
224 it('Should uninstall the plugin again', async function () {
225 const res = await cliCommand.execWithEnv(`${cmd} plugins uninstall --npm-name peertube-plugin-hello-world`)
226
227 expect(res).to.not.contain('peertube-plugin-hello-world')
228 })
210 }) 229 })
211 230
212 describe('Manage video redundancies', function () { 231 describe('Manage video redundancies', function () {
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts
index ae625114d..9dd3f08c9 100644
--- a/server/tools/peertube-plugins.ts
+++ b/server/tools/peertube-plugins.ts
@@ -31,6 +31,7 @@ program
31 .option('-p, --password <token>', 'Password') 31 .option('-p, --password <token>', 'Password')
32 .option('-P --path <path>', 'Install from a path') 32 .option('-P --path <path>', 'Install from a path')
33 .option('-n, --npm-name <npmName>', 'Install from npm') 33 .option('-n, --npm-name <npmName>', 'Install from npm')
34 .option('--plugin-version <pluginVersion>', 'Specify the plugin version to install (only available when installing from npm)')
34 .action((options, command) => installPluginCLI(command, options)) 35 .action((options, command) => installPluginCLI(command, options))
35 36
36program 37program
@@ -109,7 +110,7 @@ async function installPluginCLI (command: Command, options: OptionValues) {
109 await assignToken(server, username, password) 110 await assignToken(server, username, password)
110 111
111 try { 112 try {
112 await server.plugins.install({ npmName: options.npmName, path: options.path }) 113 await server.plugins.install({ npmName: options.npmName, path: options.path, pluginVersion: options.pluginVersion })
113 } catch (err) { 114 } catch (err) {
114 console.error('Cannot install plugin.', err) 115 console.error('Cannot install plugin.', err)
115 process.exit(-1) 116 process.exit(-1)
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
index b944475a2..9bf24afff 100644
--- a/shared/extra-utils/server/plugins-command.ts
+++ b/shared/extra-utils/server/plugins-command.ts
@@ -158,15 +158,16 @@ export class PluginsCommand extends AbstractCommand {
158 install (options: OverrideCommandOptions & { 158 install (options: OverrideCommandOptions & {
159 path?: string 159 path?: string
160 npmName?: string 160 npmName?: string
161 pluginVersion?: string
161 }) { 162 }) {
162 const { npmName, path } = options 163 const { npmName, path, pluginVersion } = options
163 const apiPath = '/api/v1/plugins/install' 164 const apiPath = '/api/v1/plugins/install'
164 165
165 return this.postBodyRequest({ 166 return this.postBodyRequest({
166 ...options, 167 ...options,
167 168
168 path: apiPath, 169 path: apiPath,
169 fields: { npmName, path }, 170 fields: { npmName, path, pluginVersion },
170 implicitToken: true, 171 implicitToken: true,
171 defaultExpectedStatus: HttpStatusCode.OK_200 172 defaultExpectedStatus: HttpStatusCode.OK_200
172 }) 173 })
diff --git a/shared/models/plugins/client/plugin-selector-id.type.ts b/shared/models/plugins/client/plugin-selector-id.type.ts
index b74dffbef..8d23314b5 100644
--- a/shared/models/plugins/client/plugin-selector-id.type.ts
+++ b/shared/models/plugins/client/plugin-selector-id.type.ts
@@ -1 +1,10 @@
1export type PluginSelectorId = 'login-form' 1export type PluginSelectorId =
2 'login-form' |
3 'menu-user-dropdown-language-item' |
4 'about-instance-features' |
5 'about-instance-statistics' |
6 'about-instance-moderation' |
7 'about-menu-instance' |
8 'about-menu-peertube' |
9 'about-menu-network' |
10 'about-instance-other-information'
diff --git a/shared/models/plugins/server/api/install-plugin.model.ts b/shared/models/plugins/server/api/install-plugin.model.ts
index 5a268ebe1..a1d009a00 100644
--- a/shared/models/plugins/server/api/install-plugin.model.ts
+++ b/shared/models/plugins/server/api/install-plugin.model.ts
@@ -1,4 +1,5 @@
1export interface InstallOrUpdatePlugin { 1export interface InstallOrUpdatePlugin {
2 npmName?: string 2 npmName?: string
3 pluginVersion?: string
3 path?: string 4 path?: string
4} 5}
diff --git a/support/doc/development/ci.md b/support/doc/development/ci.md
new file mode 100644
index 000000000..7d6eef197
--- /dev/null
+++ b/support/doc/development/ci.md
@@ -0,0 +1,40 @@
1# Continuous integration
2
3PeerTube uses Github Actions as a CI platform.
4CI tasks are described in `.github/workflows`.
5
6## benchmark.yml
7
8*Scheduled*
9
10Run various benchmarks (build, API etc) and upload results on https://builds.joinpeertube.org/peertube-stats/ to be publicly consumed.
11
12## codeql.yml
13
14*Scheduled, on push on develop and on pull request*
15
16Run CodeQL task to throw code security issues in Github. https://lgtm.com/projects/g/Chocobozzz/PeerTube can also be used.
17
18## docker.yml
19
20*Scheduled and on push on master*
21
22Build `chocobozzz/peertube-webserver:latest`, `chocobozzz/peertube:production-...`, `chocobozzz/peertube:v-...` (only latest PeerTube tag) and `chocobozzz/peertube:develop-...` Docker images. Scheduled to automatically upgrade image software (Debian security issues etc).
23
24## nightly.yml
25
26*Scheduled*
27
28Build PeerTube nightly build (`develop` branch) and upload the release on https://builds.joinpeertube.org/nightly.
29
30## stats.yml
31
32*On push on develop*
33
34Create various PeerTube stats (line of codes, build size, lighthouse report) and upload results on https://builds.joinpeertube.org/peertube-stats/ to be publicly consumed.
35
36## test.yml
37
38*Scheduled, on push and pull request*
39
40Run PeerTube lint and tests.
diff --git a/yarn.lock b/yarn.lock
index ee35c5c7e..62c95cfff 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2613,11 +2613,6 @@ call-me-maybe@^1.0.1:
2613 resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" 2613 resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
2614 integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= 2614 integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
2615 2615
2616callsite@^1.0.0:
2617 version "1.0.0"
2618 resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
2619 integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
2620
2621callsites@^3.0.0: 2616callsites@^3.0.0:
2622 version "3.1.0" 2617 version "3.1.0"
2623 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 2618 resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
@@ -3229,13 +3224,6 @@ debuglog@^1.0.0:
3229 resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" 3224 resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
3230 integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= 3225 integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
3231 3226
3232decache@^4.6.0:
3233 version "4.6.0"
3234 resolved "https://registry.yarnpkg.com/decache/-/decache-4.6.0.tgz#87026bc6e696759e82d57a3841c4e251a30356e8"
3235 integrity sha512-PppOuLiz+DFeaUvFXEYZjLxAkKiMYH/do/b/MxpDe/8AgKBi5GhZxridoVIbBq72GDbL36e4p0Ce2jTGUwwU+w==
3236 dependencies:
3237 callsite "^1.0.0"
3238
3239decamelize@^1.2.0: 3227decamelize@^1.2.0:
3240 version "1.2.0" 3228 version "1.2.0"
3241 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 3229 resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"