mirror of
https://github.com/bolucat/Archive.git
synced 2025-12-24 13:28:37 +08:00
Update On Sat Dec 6 19:36:47 CET 2025
This commit is contained in:
1
.github/update.log
vendored
1
.github/update.log
vendored
@@ -1203,3 +1203,4 @@ Update On Tue Dec 2 19:43:40 CET 2025
|
|||||||
Update On Wed Dec 3 19:42:45 CET 2025
|
Update On Wed Dec 3 19:42:45 CET 2025
|
||||||
Update On Thu Dec 4 19:44:01 CET 2025
|
Update On Thu Dec 4 19:44:01 CET 2025
|
||||||
Update On Fri Dec 5 19:41:34 CET 2025
|
Update On Fri Dec 5 19:41:34 CET 2025
|
||||||
|
Update On Sat Dec 6 19:36:39 CET 2025
|
||||||
|
|||||||
@@ -197,6 +197,10 @@ func newSearcher(major int) *searcher {
|
|||||||
case 12:
|
case 12:
|
||||||
fallthrough
|
fallthrough
|
||||||
case 13:
|
case 13:
|
||||||
|
fallthrough
|
||||||
|
case 14:
|
||||||
|
fallthrough
|
||||||
|
case 15:
|
||||||
s = &searcher{
|
s = &searcher{
|
||||||
headSize: 64,
|
headSize: 64,
|
||||||
tcpItemSize: 744,
|
tcpItemSize: 744,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"manifest_version": 1,
|
"manifest_version": 1,
|
||||||
"latest": {
|
"latest": {
|
||||||
"mihomo": "v1.19.17",
|
"mihomo": "v1.19.17",
|
||||||
"mihomo_alpha": "alpha-6539b50",
|
"mihomo_alpha": "alpha-f44aa22",
|
||||||
"clash_rs": "v0.9.2",
|
"clash_rs": "v0.9.2",
|
||||||
"clash_premium": "2023-09-05-gdcc8d87",
|
"clash_premium": "2023-09-05-gdcc8d87",
|
||||||
"clash_rs_alpha": "0.9.2-alpha+sha.e1f8fbb"
|
"clash_rs_alpha": "0.9.2-alpha+sha.81f5ac5"
|
||||||
},
|
},
|
||||||
"arch_template": {
|
"arch_template": {
|
||||||
"mihomo": {
|
"mihomo": {
|
||||||
@@ -69,5 +69,5 @@
|
|||||||
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
|
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"updated_at": "2025-12-04T22:21:10.965Z"
|
"updated_at": "2025-12-05T22:21:23.531Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,23 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.51.0](https://github.com/filebrowser/filebrowser/compare/v2.50.0...v2.51.0) (2025-12-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* update translations ([2d88c06](https://github.com/filebrowser/filebrowser/commit/2d88c067611e936056dbbf04247f1c1c709b2a09))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* added column separator select (comma, semicolon and both) in CSV viewer ([#5604](https://github.com/filebrowser/filebrowser/issues/5604)) ([204a3f0](https://github.com/filebrowser/filebrowser/commit/204a3f0eeaa0c68781b60651bf27c4b27eac44e6))
|
||||||
|
|
||||||
|
|
||||||
|
### Refactorings
|
||||||
|
|
||||||
|
* cleanup package names ([#5605](https://github.com/filebrowser/filebrowser/issues/5605)) ([f029c30](https://github.com/filebrowser/filebrowser/commit/f029c3005e450cfbebb074c42dbdf65db9c8d56a))
|
||||||
|
|
||||||
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
|
## [2.50.0](https://github.com/filebrowser/filebrowser/compare/v2.49.0...v2.50.0) (2025-11-30)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
## Multistage build: First stage fetches dependencies
|
## Multistage build: First stage fetches dependencies
|
||||||
FROM alpine:3.22 AS fetcher
|
FROM alpine:3.23 AS fetcher
|
||||||
|
|
||||||
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
# install and copy ca-certificates, mailcap, and tini-static; download JSON.sh
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
@@ -146,7 +146,7 @@ func (a *HookAuth) GetValues(s string) {
|
|||||||
// SaveUser updates the existing user or creates a new one when not found
|
// SaveUser updates the existing user or creates a new one when not found
|
||||||
func (a *HookAuth) SaveUser() (*users.User, error) {
|
func (a *HookAuth) SaveUser() (*users.User, error) {
|
||||||
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
u, err := a.Users.Get(a.Server.Root, a.Cred.Username)
|
||||||
if err != nil && !errors.Is(err, fbErrors.ErrNotExist) {
|
if err != nil && !errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
@@ -21,7 +21,7 @@ type ProxyAuth struct {
|
|||||||
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
func (a ProxyAuth) Auth(r *http.Request, usr users.Store, setting *settings.Settings, srv *settings.Server) (*users.User, error) {
|
||||||
username := r.Header.Get(a.Header)
|
username := r.Header.Get(a.Header)
|
||||||
user, err := usr.Get(srv.Root, username)
|
user, err := usr.Get(srv.Root, username)
|
||||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
if errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return a.createUser(usr, setting, srv, username)
|
return a.createUser(usr, setting, srv, username)
|
||||||
}
|
}
|
||||||
return user, err
|
return user, err
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
nerrors "errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (a
|
|||||||
}
|
}
|
||||||
|
|
||||||
if header == "" {
|
if header == "" {
|
||||||
return nil, nerrors.New("you must set the flag 'auth.header' for method 'proxy'")
|
return nil, errors.New("you must set the flag 'auth.header' for method 'proxy'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &auth.ProxyAuth{Header: header}, nil
|
return &auth.ProxyAuth{Header: header}, nil
|
||||||
@@ -163,7 +163,7 @@ func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
|
|||||||
}
|
}
|
||||||
|
|
||||||
if command == "" {
|
if command == "" {
|
||||||
return nil, nerrors.New("you must set the flag 'auth.command' for method 'hook'")
|
return nil, errors.New("you must set the flag 'auth.command' for method 'hook'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &auth.HookAuth{Command: command}, nil
|
return &auth.HookAuth{Command: command}, nil
|
||||||
@@ -186,7 +186,7 @@ func getAuthentication(flags *pflag.FlagSet, defaults ...interface{}) (settings.
|
|||||||
case auth.MethodHookAuth:
|
case auth.MethodHookAuth:
|
||||||
auther, err = getHookAuth(flags, defaultAuther)
|
auther, err = getHookAuth(flags, defaultAuther)
|
||||||
default:
|
default:
|
||||||
return "", nil, errors.ErrInvalidAuthMethod
|
return "", nil, fberrors.ErrInvalidAuthMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,7 +361,7 @@ func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Ser
|
|||||||
flags.Visit(visit)
|
flags.Visit(visit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := nerrors.Join(errs...)
|
err := errors.Join(errs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package errors
|
package fberrors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ func stat(opts *FileOptions) (*FileInfo, error) {
|
|||||||
// algorithm. The checksums data is saved on File object.
|
// algorithm. The checksums data is saved on File object.
|
||||||
func (i *FileInfo) Checksum(algo string) error {
|
func (i *FileInfo) Checksum(algo string) error {
|
||||||
if i.IsDir {
|
if i.IsDir {
|
||||||
return fbErrors.ErrIsDirectory
|
return fberrors.ErrIsDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
if i.Checksums == nil {
|
if i.Checksums == nil {
|
||||||
@@ -193,7 +193,7 @@ func (i *FileInfo) Checksum(algo string) error {
|
|||||||
case "sha512":
|
case "sha512":
|
||||||
h = sha512.New()
|
h = sha512.New()
|
||||||
default:
|
default:
|
||||||
return fbErrors.ErrInvalidOption
|
return fberrors.ErrInvalidOption
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(h, reader)
|
_, err = io.Copy(h, reader)
|
||||||
|
|||||||
@@ -25,9 +25,29 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-if="data.rows.length > 100" class="csv-info">
|
<div class="csv-footer">
|
||||||
<i class="material-icons">info</i>
|
<div class="csv-info" v-if="data.rows.length > 100">
|
||||||
<span>Showing {{ data.rows.length }} rows</span>
|
<i class="material-icons">info</i>
|
||||||
|
<span>Showing {{ data.rows.length }} rows</span>
|
||||||
|
</div>
|
||||||
|
<div class="column-separator">
|
||||||
|
<label for="columnSeparator">Column Separator</label>
|
||||||
|
<select
|
||||||
|
id="columnSeparator"
|
||||||
|
class="input input--block"
|
||||||
|
v-model="columnSeparator"
|
||||||
|
>
|
||||||
|
<option :value="[',']">
|
||||||
|
{{ $t("available_csv_separators.comma") }}
|
||||||
|
</option>
|
||||||
|
<option :value="[';']">
|
||||||
|
{{ $t("available_csv_separators.semicolon") }}
|
||||||
|
</option>
|
||||||
|
<option :value="[',', ';']">
|
||||||
|
{{ $t("available_csv_separators.both") }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +55,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parseCSV, type CsvData } from "@/utils/csv";
|
import { parseCSV, type CsvData } from "@/utils/csv";
|
||||||
import { computed } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: string;
|
content: string;
|
||||||
@@ -46,9 +66,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
error: "",
|
error: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const columnSeparator = ref([","]);
|
||||||
|
|
||||||
const data = computed<CsvData>(() => {
|
const data = computed<CsvData>(() => {
|
||||||
try {
|
try {
|
||||||
return parseCSV(props.content);
|
return parseCSV(props.content, columnSeparator.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse CSV:", e);
|
console.error("Failed to parse CSV:", e);
|
||||||
return { headers: [], rows: [] };
|
return { headers: [], rows: [] };
|
||||||
@@ -181,6 +203,18 @@ const displayError = computed(() => {
|
|||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.csv-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.csv-footer > :only-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.csv-info {
|
.csv-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -194,6 +228,21 @@ const displayError = computed(() => {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-separator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-separator > label {
|
||||||
|
font-size: small;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-separator > select {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.csv-info i {
|
.csv-info i {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "دقائق",
|
"minutes": "دقائق",
|
||||||
"seconds": "ثواني",
|
"seconds": "ثواني",
|
||||||
"unit": "وحدة الوقت"
|
"unit": "وحدة الوقت"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Минути",
|
"minutes": "Минути",
|
||||||
"seconds": "Секунди",
|
"seconds": "Секунди",
|
||||||
"unit": "Единица за време"
|
"unit": "Единица за време"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuts",
|
"minutes": "Minuts",
|
||||||
"seconds": "Segons",
|
"seconds": "Segons",
|
||||||
"unit": "Unitat"
|
"unit": "Unitat"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuty",
|
"minutes": "Minuty",
|
||||||
"seconds": "Sekundy",
|
"seconds": "Sekundy",
|
||||||
"unit": "Časová jednotka"
|
"unit": "Časová jednotka"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"openFile": "Datei öffnen",
|
"openFile": "Datei öffnen",
|
||||||
"discardChanges": "Verwerfen",
|
"discardChanges": "Verwerfen",
|
||||||
"saveChanges": "Save changes",
|
"saveChanges": "Änderungen speichern",
|
||||||
"editAsText": "Edit as Text"
|
"editAsText": "Als Text bearbeiten"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"downloadFile": "Download Datei",
|
"downloadFile": "Download Datei",
|
||||||
@@ -77,8 +77,8 @@
|
|||||||
"sortByName": "Nach Namen sortieren",
|
"sortByName": "Nach Namen sortieren",
|
||||||
"sortBySize": "Nach Größe sortieren",
|
"sortBySize": "Nach Größe sortieren",
|
||||||
"noPreview": "Für diese Datei ist keine Vorschau verfügbar.",
|
"noPreview": "Für diese Datei ist keine Vorschau verfügbar.",
|
||||||
"csvTooLarge": "CSV file is too large for preview (>5MB). Please download to view.",
|
"csvTooLarge": "Die CSV-Datei ist zu groß für die Vorschau (>5 MB). Bitte herunterladen, um sie anzuzeigen.",
|
||||||
"csvLoadFailed": "Failed to load CSV file."
|
"csvLoadFailed": "Fehler beim Laden der CSV-Datei."
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"click": "Wähle Datei oder Ordner",
|
"click": "Wähle Datei oder Ordner",
|
||||||
@@ -105,9 +105,9 @@
|
|||||||
"username": "Benutzername",
|
"username": "Benutzername",
|
||||||
"usernameTaken": "Benutzername ist bereits vergeben",
|
"usernameTaken": "Benutzername ist bereits vergeben",
|
||||||
"wrongCredentials": "Falsche Zugangsdaten",
|
"wrongCredentials": "Falsche Zugangsdaten",
|
||||||
"passwordTooShort": "Password must be at least {min} characters",
|
"passwordTooShort": "Passwort muss mindestens {min} Zeichen lang sein",
|
||||||
"logout_reasons": {
|
"logout_reasons": {
|
||||||
"inactivity": "You have been logged out due to inactivity."
|
"inactivity": "Du wurdest aufgrund von Inaktivität abgemeldet."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
"video": "Video"
|
"video": "Video"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"aceEditorTheme": "Ace editor theme",
|
"aceEditorTheme": "Ace Editor Theme",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"administrator": "Administrator",
|
"administrator": "Administrator",
|
||||||
"allowCommands": "Befehle ausführen",
|
"allowCommands": "Befehle ausführen",
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
"allowNew": "Erstellen neuer Dateien und Ordner",
|
"allowNew": "Erstellen neuer Dateien und Ordner",
|
||||||
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
|
"allowPublish": "Veröffentlichen von neuen Beiträgen und Seiten",
|
||||||
"allowSignup": "Erlaube Benutzern sich zu registrieren",
|
"allowSignup": "Erlaube Benutzern sich zu registrieren",
|
||||||
"hideLoginButton": "Hide the login button from public pages",
|
"hideLoginButton": "Den Login-Button auf öffentlichen Seiten ausblenden",
|
||||||
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
|
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
|
||||||
"branding": "Design",
|
"branding": "Design",
|
||||||
"brandingDirectoryPath": "Designverzeichnispfad",
|
"brandingDirectoryPath": "Designverzeichnispfad",
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.",
|
"commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariable {0} und {1} sind verfügbar, wobei {0} relative zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.",
|
||||||
"commandsUpdated": "Befehle aktualisiert!",
|
"commandsUpdated": "Befehle aktualisiert!",
|
||||||
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
|
"createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer",
|
||||||
"minimumPasswordLength": "Minimum password length",
|
"minimumPasswordLength": "Mindestlänge für Passwörter",
|
||||||
"tusUploads": "Gestückelter Upload",
|
"tusUploads": "Gestückelter Upload",
|
||||||
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
|
"tusUploadsHelp": "File Browser unterstützt das Hochladen von gestückelten Dateien und ermöglicht so einen effizienten, zuverlässigen, fortsetzbaren und gestückelten Datei-Upload auch in unzuverlässigen Netzwerken.",
|
||||||
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
|
"tusUploadsChunkSize": "Gibt die maximale Größe pro Anfrage an (direkte Uploads werden für kleinere Uploads verwendet). Bitte geben Sie eine Byte-Angabe oder eine Zeichenfolge wie 10 MB, 1 GB usw. an",
|
||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuten",
|
"minutes": "Minuten",
|
||||||
"seconds": "Sekunden",
|
"seconds": "Sekunden",
|
||||||
"unit": "Zeiteinheit"
|
"unit": "Zeiteinheit"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Λεπτά",
|
"minutes": "Λεπτά",
|
||||||
"seconds": "Δευτερόλεπτα",
|
"seconds": "Δευτερόλεπτα",
|
||||||
"unit": "Μονάδα χρόνου"
|
"unit": "Μονάδα χρόνου"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
"seconds": "Seconds",
|
"seconds": "Seconds",
|
||||||
"unit": "Time Unit"
|
"unit": "Time Unit"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutos",
|
"minutes": "Minutos",
|
||||||
"seconds": "Segundos",
|
"seconds": "Segundos",
|
||||||
"unit": "Unidad"
|
"unit": "Unidad"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "دقیقه",
|
"minutes": "دقیقه",
|
||||||
"seconds": "ثانیه",
|
"seconds": "ثانیه",
|
||||||
"unit": "واحد زمان"
|
"unit": "واحد زمان"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutes",
|
"minutes": "Minutes",
|
||||||
"seconds": "Secondes",
|
"seconds": "Secondes",
|
||||||
"unit": "Unité de temps"
|
"unit": "Unité de temps"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "דקות",
|
"minutes": "דקות",
|
||||||
"seconds": "שניות",
|
"seconds": "שניות",
|
||||||
"unit": "יחידת זמן"
|
"unit": "יחידת זמן"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minute",
|
"minutes": "Minute",
|
||||||
"seconds": "Sekunde",
|
"seconds": "Sekunde",
|
||||||
"unit": "Jedinica vremena"
|
"unit": "Jedinica vremena"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Perc",
|
"minutes": "Perc",
|
||||||
"seconds": "Másodperc",
|
"seconds": "Másodperc",
|
||||||
"unit": "Időegység"
|
"unit": "Időegység"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Mínútur",
|
"minutes": "Mínútur",
|
||||||
"seconds": "Sekúndur",
|
"seconds": "Sekúndur",
|
||||||
"unit": "Tímastilling"
|
"unit": "Tímastilling"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuti",
|
"minutes": "Minuti",
|
||||||
"seconds": "Secondi",
|
"seconds": "Secondi",
|
||||||
"unit": "Unità di tempo"
|
"unit": "Unità di tempo"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "分",
|
"minutes": "分",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"unit": "時間の単位"
|
"unit": "時間の単位"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "분",
|
"minutes": "분",
|
||||||
"seconds": "초",
|
"seconds": "초",
|
||||||
"unit": "Time Unit"
|
"unit": "Time Unit"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuten",
|
"minutes": "Minuten",
|
||||||
"seconds": "Seconden",
|
"seconds": "Seconden",
|
||||||
"unit": "Tijdseenheid"
|
"unit": "Tijdseenheid"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutt",
|
"minutes": "Minutt",
|
||||||
"seconds": "Sekunder",
|
"seconds": "Sekunder",
|
||||||
"unit": "Time format"
|
"unit": "Time format"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuty",
|
"minutes": "Minuty",
|
||||||
"seconds": "Sekundy",
|
"seconds": "Sekundy",
|
||||||
"unit": "Jednostka czasu"
|
"unit": "Jednostka czasu"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutos",
|
"minutes": "Minutos",
|
||||||
"seconds": "Segundos",
|
"seconds": "Segundos",
|
||||||
"unit": "Unidades de Tempo"
|
"unit": "Unidades de Tempo"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minutos",
|
"minutes": "Minutos",
|
||||||
"seconds": "Segundos",
|
"seconds": "Segundos",
|
||||||
"unit": "Unidades de tempo"
|
"unit": "Unidades de tempo"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minute",
|
"minutes": "Minute",
|
||||||
"seconds": "Secunde",
|
"seconds": "Secunde",
|
||||||
"unit": "Unitate de timp"
|
"unit": "Unitate de timp"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Минуты",
|
"minutes": "Минуты",
|
||||||
"seconds": "Секунды",
|
"seconds": "Секунды",
|
||||||
"unit": "Единица времени"
|
"unit": "Единица времени"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minúty",
|
"minutes": "Minúty",
|
||||||
"seconds": "Sekundy",
|
"seconds": "Sekundy",
|
||||||
"unit": "Jednotka času"
|
"unit": "Jednotka času"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Minuter",
|
"minutes": "Minuter",
|
||||||
"seconds": "Sekunder",
|
"seconds": "Sekunder",
|
||||||
"unit": "Tidsenhet"
|
"unit": "Tidsenhet"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Dakika",
|
"minutes": "Dakika",
|
||||||
"seconds": "Saniye",
|
"seconds": "Saniye",
|
||||||
"unit": "Zaman birimi"
|
"unit": "Zaman birimi"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Хвилини",
|
"minutes": "Хвилини",
|
||||||
"seconds": "Секунди",
|
"seconds": "Секунди",
|
||||||
"unit": "Одиниця часу"
|
"unit": "Одиниця часу"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "Phút",
|
"minutes": "Phút",
|
||||||
"seconds": "Giây",
|
"seconds": "Giây",
|
||||||
"unit": "Đơn vị"
|
"unit": "Đơn vị"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "分钟",
|
"minutes": "分钟",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"unit": "时间单位"
|
"unit": "时间单位"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,5 +272,10 @@
|
|||||||
"minutes": "分鐘",
|
"minutes": "分鐘",
|
||||||
"seconds": "秒",
|
"seconds": "秒",
|
||||||
"unit": "時間單位"
|
"unit": "時間單位"
|
||||||
|
},
|
||||||
|
"available_csv_separators": {
|
||||||
|
"comma": "Comma (,)",
|
||||||
|
"semicolon": "Semicolon (;)",
|
||||||
|
"both": "Both (,) and (;)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ export interface CsvData {
|
|||||||
* Parse CSV content into headers and rows
|
* Parse CSV content into headers and rows
|
||||||
* Supports quoted fields and handles commas within quotes
|
* Supports quoted fields and handles commas within quotes
|
||||||
*/
|
*/
|
||||||
export function parseCSV(content: string): CsvData {
|
export function parseCSV(
|
||||||
|
content: string,
|
||||||
|
columnSeparator: Array<string>
|
||||||
|
): CsvData {
|
||||||
if (!content || content.trim().length === 0) {
|
if (!content || content.trim().length === 0) {
|
||||||
return { headers: [], rows: [] };
|
return { headers: [], rows: [] };
|
||||||
}
|
}
|
||||||
@@ -35,7 +38,7 @@ export function parseCSV(content: string): CsvData {
|
|||||||
// Toggle quote state
|
// Toggle quote state
|
||||||
inQuotes = !inQuotes;
|
inQuotes = !inQuotes;
|
||||||
}
|
}
|
||||||
} else if (char === "," && !inQuotes) {
|
} else if (columnSeparator.includes(char) && !inQuotes) {
|
||||||
// Field separator
|
// Field separator
|
||||||
row.push(currentField);
|
row.push(currentField);
|
||||||
currentField = "";
|
currentField = "";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ require (
|
|||||||
github.com/samber/lo v1.52.0
|
github.com/samber/lo v1.52.0
|
||||||
github.com/shirou/gopsutil/v4 v4.25.11
|
github.com/shirou/gopsutil/v4 v4.25.11
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
|||||||
@@ -214,8 +214,8 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
|||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5/request"
|
"github.com/golang-jwt/jwt/v5/request"
|
||||||
|
|
||||||
fbAuth "github.com/filebrowser/filebrowser/v2/auth"
|
fbAuth "github.com/filebrowser/filebrowser/v2/auth"
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
@@ -185,7 +185,7 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int,
|
|||||||
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
|
||||||
|
|
||||||
err = d.store.Users.Save(user)
|
err = d.store.Users.Save(user)
|
||||||
if errors.Is(err, fbErrors.ErrExist) {
|
if errors.Is(err, fberrors.ErrExist) {
|
||||||
return http.StatusConflict, err
|
return http.StatusConflict, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
|
|
||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
// global headers to append to every response
|
// global headers to append to every response
|
||||||
var globalHeaders = map[string]string{
|
var globalHeaders = map[string]string{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//go:generate go-enum --sql --marshal --names --file $GOFILE
|
//go:generate go-enum --sql --marshal --names --file $GOFILE
|
||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Code generated by go-enum
|
// Code generated by go-enum
|
||||||
// DO NOT EDIT!
|
// DO NOT EDIT!
|
||||||
|
|
||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/fileutils"
|
"github.com/filebrowser/filebrowser/v2/fileutils"
|
||||||
)
|
)
|
||||||
@@ -44,7 +44,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
|
|||||||
|
|
||||||
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
if checksum := r.URL.Query().Get("checksum"); checksum != "" {
|
||||||
err := file.Checksum(checksum)
|
err := file.Checksum(checksum)
|
||||||
if errors.Is(err, fbErrors.ErrInvalidOption) {
|
if errors.Is(err, fberrors.ErrInvalidOption) {
|
||||||
return http.StatusBadRequest, nil
|
return http.StatusBadRequest, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
@@ -238,7 +238,7 @@ func checkParent(src, dst string) error {
|
|||||||
|
|
||||||
rel = filepath.ToSlash(rel)
|
rel = filepath.ToSlash(rel)
|
||||||
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
if !strings.HasPrefix(rel, "../") && rel != ".." && rel != "." {
|
||||||
return fbErrors.ErrSourceIsParent
|
return fberrors.ErrSourceIsParent
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -304,13 +304,13 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
|||||||
switch action {
|
switch action {
|
||||||
case "copy":
|
case "copy":
|
||||||
if !d.user.Perm.Create {
|
if !d.user.Perm.Create {
|
||||||
return fbErrors.ErrPermissionDenied
|
return fberrors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
|
return fileutils.Copy(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
|
||||||
case "rename":
|
case "rename":
|
||||||
if !d.user.Perm.Rename {
|
if !d.user.Perm.Rename {
|
||||||
return fbErrors.ErrPermissionDenied
|
return fberrors.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
src = path.Clean("/" + src)
|
src = path.Clean("/" + src)
|
||||||
dst = path.Clean("/" + dst)
|
dst = path.Clean("/" + dst)
|
||||||
@@ -335,7 +335,7 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
|
|||||||
|
|
||||||
return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
|
return fileutils.MoveFile(d.user.Fs, src, dst, d.settings.FileMode, d.settings.DirMode)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported action %s: %w", action, fbErrors.ErrInvalidRequestParams)
|
return fmt.Errorf("unsupported action %s: %w", action, fberrors.ErrInvalidRequestParams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/share"
|
"github.com/filebrowser/filebrowser/v2/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ var shareListHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
|||||||
} else {
|
} else {
|
||||||
s, err = d.store.Share.FindByUserID(d.user.ID)
|
s, err = d.store.Share.FindByUserID(d.user.ID)
|
||||||
}
|
}
|
||||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
if errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return renderJSON(w, r, []*share.Link{})
|
return renderJSON(w, r, []*share.Link{})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ var shareListHandler = withPermShare(func(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var shareGetsHandler = withPermShare(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
s, err := d.store.Share.Gets(r.URL.Path, d.user.ID)
|
s, err := d.store.Share.Gets(r.URL.Path, d.user.ID)
|
||||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
if errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return renderJSON(w, r, []*share.Link{})
|
return renderJSON(w, r, []*share.Link{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func getUserID(r *http.Request) (uint, error) {
|
|||||||
|
|
||||||
func getUser(_ http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
|
func getUser(_ http.ResponseWriter, r *http.Request) (*modifyUserRequest, error) {
|
||||||
if r.Body == nil {
|
if r.Body == nil {
|
||||||
return nil, fbErrors.ErrEmptyRequest
|
return nil, fberrors.ErrEmptyRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &modifyUserRequest{}
|
req := &modifyUserRequest{}
|
||||||
@@ -46,7 +46,7 @@ func getUser(_ http.ResponseWriter, r *http.Request) (*modifyUserRequest, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.What != "user" {
|
if req.What != "user" {
|
||||||
return nil, fbErrors.ErrInvalidDataType
|
return nil, fberrors.ErrInvalidDataType
|
||||||
}
|
}
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
@@ -87,7 +87,7 @@ var usersGetHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
|||||||
|
|
||||||
var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
var userGetHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
|
||||||
u, err := d.store.Users.Get(d.server.Root, d.raw.(uint))
|
u, err := d.store.Users.Get(d.server.Root, d.raw.(uint))
|
||||||
if errors.Is(err, fbErrors.ErrNotExist) {
|
if errors.Is(err, fberrors.ErrNotExist) {
|
||||||
return http.StatusNotFound, err
|
return http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Data.Password == "" {
|
if req.Data.Password == "" {
|
||||||
return http.StatusBadRequest, fbErrors.ErrEmptyPassword
|
return http.StatusBadRequest, fberrors.ErrEmptyPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Data.Password, err = users.ValidateAndHashPwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
req.Data.Password, err = users.ValidateAndHashPwd(req.Data.Password, d.settings.MinimumPasswordLength)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package http
|
package fbhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,7 @@ var defaultEvents = []string{
|
|||||||
// Save saves the settings for the current instance.
|
// Save saves the settings for the current instance.
|
||||||
func (s *Storage) Save(set *Settings) error {
|
func (s *Storage) Save(set *Settings) error {
|
||||||
if len(set.Key) == 0 {
|
if len(set.Key) == 0 {
|
||||||
return errors.ErrEmptyKey
|
return fberrors.ErrEmptyKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if set.Defaults.Locale == "" {
|
if set.Defaults.Locale == "" {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package share
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageBackend is the interface to implement for a share storage.
|
// StorageBackend is the interface to implement for a share storage.
|
||||||
@@ -79,7 +79,7 @@ func (s *Storage) GetByHash(hash string) (*Link, error) {
|
|||||||
if err := s.Delete(link.Hash); err != nil {
|
if err := s.Delete(link.Hash); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, errors.ErrNotExist
|
return nil, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return link, nil
|
return link, nil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/auth"
|
"github.com/filebrowser/filebrowser/v2/auth"
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/settings"
|
"github.com/filebrowser/filebrowser/v2/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ func (s authBackend) Get(t settings.AuthMethod) (auth.Auther, error) {
|
|||||||
case auth.MethodNoAuth:
|
case auth.MethodNoAuth:
|
||||||
auther = &auth.NoAuth{}
|
auther = &auth.NoAuth{}
|
||||||
default:
|
default:
|
||||||
return nil, errors.ErrInvalidAuthMethod
|
return nil, fberrors.ErrInvalidAuthMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
return auther, get(s.db, "auther", auther)
|
return auther, get(s.db, "auther", auther)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/asdine/storm/v3/q"
|
"github.com/asdine/storm/v3/q"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/share"
|
"github.com/filebrowser/filebrowser/v2/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ func (s shareBackend) All() ([]*share.Link, error) {
|
|||||||
var v []*share.Link
|
var v []*share.Link
|
||||||
err := s.db.All(&v)
|
err := s.db.All(&v)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return v, fbErrors.ErrNotExist
|
return v, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, err
|
return v, err
|
||||||
@@ -28,7 +28,7 @@ func (s shareBackend) FindByUserID(id uint) ([]*share.Link, error) {
|
|||||||
var v []*share.Link
|
var v []*share.Link
|
||||||
err := s.db.Select(q.Eq("UserID", id)).Find(&v)
|
err := s.db.Select(q.Eq("UserID", id)).Find(&v)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return v, fbErrors.ErrNotExist
|
return v, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, err
|
return v, err
|
||||||
@@ -38,7 +38,7 @@ func (s shareBackend) GetByHash(hash string) (*share.Link, error) {
|
|||||||
var v share.Link
|
var v share.Link
|
||||||
err := s.db.One("Hash", hash, &v)
|
err := s.db.One("Hash", hash, &v)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return nil, fbErrors.ErrNotExist
|
return nil, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return &v, err
|
return &v, err
|
||||||
@@ -48,7 +48,7 @@ func (s shareBackend) GetPermanent(path string, id uint) (*share.Link, error) {
|
|||||||
var v share.Link
|
var v share.Link
|
||||||
err := s.db.Select(q.Eq("Path", path), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v)
|
err := s.db.Select(q.Eq("Path", path), q.Eq("Expire", 0), q.Eq("UserID", id)).First(&v)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return nil, fbErrors.ErrNotExist
|
return nil, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return &v, err
|
return &v, err
|
||||||
@@ -58,7 +58,7 @@ func (s shareBackend) Gets(path string, id uint) ([]*share.Link, error) {
|
|||||||
var v []*share.Link
|
var v []*share.Link
|
||||||
err := s.db.Select(q.Eq("Path", path), q.Eq("UserID", id)).Find(&v)
|
err := s.db.Select(q.Eq("Path", path), q.Eq("UserID", id)).Find(&v)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return v, fbErrors.ErrNotExist
|
return v, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, err
|
return v, err
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/users"
|
"github.com/filebrowser/filebrowser/v2/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,14 +25,14 @@ func (st usersBackend) GetBy(i interface{}) (user *users.User, err error) {
|
|||||||
case string:
|
case string:
|
||||||
arg = "Username"
|
arg = "Username"
|
||||||
default:
|
default:
|
||||||
return nil, fbErrors.ErrInvalidDataType
|
return nil, fberrors.ErrInvalidDataType
|
||||||
}
|
}
|
||||||
|
|
||||||
err = st.db.One(arg, i, user)
|
err = st.db.One(arg, i, user)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return nil, fbErrors.ErrNotExist
|
return nil, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func (st usersBackend) Gets() ([]*users.User, error) {
|
|||||||
var allUsers []*users.User
|
var allUsers []*users.User
|
||||||
err := st.db.All(&allUsers)
|
err := st.db.All(&allUsers)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return nil, fbErrors.ErrNotExist
|
return nil, fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,7 +76,7 @@ func (st usersBackend) Update(user *users.User, fields ...string) error {
|
|||||||
func (st usersBackend) Save(user *users.User) error {
|
func (st usersBackend) Save(user *users.User) error {
|
||||||
err := st.db.Save(user)
|
err := st.db.Save(user)
|
||||||
if errors.Is(err, storm.ErrAlreadyExists) {
|
if errors.Is(err, storm.ErrAlreadyExists) {
|
||||||
return fbErrors.ErrExist
|
return fberrors.ErrExist
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import (
|
|||||||
|
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func get(db *storm.DB, name string, to interface{}) error {
|
func get(db *storm.DB, name string, to interface{}) error {
|
||||||
err := db.Get("config", name, to)
|
err := db.Get("config", name, to)
|
||||||
if errors.Is(err, storm.ErrNotFound) {
|
if errors.Is(err, storm.ErrNotFound) {
|
||||||
return fbErrors.ErrNotExist
|
return fberrors.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
fbErrors "github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidateAndHashPwd validates and hashes a password.
|
// ValidateAndHashPwd validates and hashes a password.
|
||||||
func ValidateAndHashPwd(password string, minimumLength uint) (string, error) {
|
func ValidateAndHashPwd(password string, minimumLength uint) (string, error) {
|
||||||
if uint(len(password)) < minimumLength {
|
if uint(len(password)) < minimumLength {
|
||||||
return "", fbErrors.ErrShortPassword{MinimumLength: minimumLength}
|
return "", fberrors.ErrShortPassword{MinimumLength: minimumLength}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := commonPasswords[password]; ok {
|
if _, ok := commonPasswords[password]; ok {
|
||||||
return "", fbErrors.ErrEasyPassword
|
return "", fberrors.ErrEasyPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
return HashPwd(password)
|
return HashPwd(password)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageBackend is the interface to implement for a users storage.
|
// StorageBackend is the interface to implement for a users storage.
|
||||||
@@ -109,16 +109,16 @@ func (s *Storage) Delete(id interface{}) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.ID == 1 {
|
if user.ID == 1 {
|
||||||
return errors.ErrRootUserDeletion
|
return fberrors.ErrRootUserDeletion
|
||||||
}
|
}
|
||||||
return s.back.DeleteByUsername(id)
|
return s.back.DeleteByUsername(id)
|
||||||
case uint:
|
case uint:
|
||||||
if id == 1 {
|
if id == 1 {
|
||||||
return errors.ErrRootUserDeletion
|
return fberrors.ErrRootUserDeletion
|
||||||
}
|
}
|
||||||
return s.back.DeleteByID(id)
|
return s.back.DeleteByID(id)
|
||||||
default:
|
default:
|
||||||
return errors.ErrInvalidDataType
|
return fberrors.ErrInvalidDataType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/filebrowser/filebrowser/v2/errors"
|
fberrors "github.com/filebrowser/filebrowser/v2/errors"
|
||||||
"github.com/filebrowser/filebrowser/v2/files"
|
"github.com/filebrowser/filebrowser/v2/files"
|
||||||
"github.com/filebrowser/filebrowser/v2/rules"
|
"github.com/filebrowser/filebrowser/v2/rules"
|
||||||
)
|
)
|
||||||
@@ -64,11 +64,11 @@ func (u *User) Clean(baseScope string, fields ...string) error {
|
|||||||
switch field {
|
switch field {
|
||||||
case "Username":
|
case "Username":
|
||||||
if u.Username == "" {
|
if u.Username == "" {
|
||||||
return errors.ErrEmptyUsername
|
return fberrors.ErrEmptyUsername
|
||||||
}
|
}
|
||||||
case "Password":
|
case "Password":
|
||||||
if u.Password == "" {
|
if u.Password == "" {
|
||||||
return errors.ErrEmptyPassword
|
return fberrors.ErrEmptyPassword
|
||||||
}
|
}
|
||||||
case "ViewMode":
|
case "ViewMode":
|
||||||
if u.ViewMode == "" {
|
if u.ViewMode == "" {
|
||||||
|
|||||||
@@ -197,6 +197,10 @@ func newSearcher(major int) *searcher {
|
|||||||
case 12:
|
case 12:
|
||||||
fallthrough
|
fallthrough
|
||||||
case 13:
|
case 13:
|
||||||
|
fallthrough
|
||||||
|
case 14:
|
||||||
|
fallthrough
|
||||||
|
case 15:
|
||||||
s = &searcher{
|
s = &searcher{
|
||||||
headSize: 64,
|
headSize: 64,
|
||||||
tcpItemSize: 744,
|
tcpItemSize: 744,
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=filebrowser
|
PKG_NAME:=filebrowser
|
||||||
PKG_VERSION:=2.50.0
|
PKG_VERSION:=2.51.0
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v${PKG_VERSION}?
|
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v${PKG_VERSION}?
|
||||||
PKG_HASH:=5947c8a8c7c8df2b2646953cfa1fdee9efac8b4415a368074acab94eacc56fd7
|
PKG_HASH:=83807c5330343a4f1201e0e061725769628d87b9a5bdd9486967a8f880ce6571
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_LICENSE_FILES:=LICENSE
|
PKG_LICENSE_FILES:=LICENSE
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ s = m:section(NamedSection, arg[1], "nodes", "")
|
|||||||
s.addremove = false
|
s.addremove = false
|
||||||
s.dynamic = false
|
s.dynamic = false
|
||||||
|
|
||||||
o = s:option(DummyValue, "passwall", " ")
|
o = s:option(DummyValue, "passwall", " ")
|
||||||
o.rawhtml = true
|
o.rawhtml = true
|
||||||
o.template = "passwall/node_list/link_share_man"
|
o.template = "passwall/node_list/link_share_man"
|
||||||
o.value = arg[1]
|
o.value = arg[1]
|
||||||
@@ -61,7 +61,7 @@ if api.is_finded("ipt2socks") then
|
|||||||
|
|
||||||
s.fields["type"]:value("Socks", translate("Socks"))
|
s.fields["type"]:value("Socks", translate("Socks"))
|
||||||
|
|
||||||
o = s:option(ListValue, _n("del_protocol")) --始终隐藏,用于删除 protocol
|
o = s:option(ListValue, _n("del_protocol"), " ") --始终隐藏,用于删除 protocol
|
||||||
o:depends({ [_n("__hide")] = "1" })
|
o:depends({ [_n("__hide")] = "1" })
|
||||||
o.rewrite_option = "protocol"
|
o.rewrite_option = "protocol"
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,10 @@ if api.is_js_luci() then
|
|||||||
uci:commit(appname)
|
uci:commit(appname)
|
||||||
api.showMsg_Redirect()
|
api.showMsg_Redirect()
|
||||||
end
|
end
|
||||||
end
|
m.render = function(self, ...)
|
||||||
|
Map.render(self, ...)
|
||||||
m.render = function(self, ...)
|
api.optimize_cbi_ui()
|
||||||
Map.render(self, ...)
|
end
|
||||||
api.optimize_cbi_ui()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- [[ Subscribe Settings ]]--
|
-- [[ Subscribe Settings ]]--
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ if api.is_js_luci() then
|
|||||||
uci:commit(appname)
|
uci:commit(appname)
|
||||||
api.showMsg_Redirect(self.redirect, 3000)
|
api.showMsg_Redirect(self.redirect, 3000)
|
||||||
end
|
end
|
||||||
end
|
m.render = function(self, ...)
|
||||||
|
Map.render(self, ...)
|
||||||
m.render = function(self, ...)
|
api.optimize_cbi_ui()
|
||||||
Map.render(self, ...)
|
end
|
||||||
api.optimize_cbi_ui()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local has_ss = api.is_finded("ss-redir")
|
local has_ss = api.is_finded("ss-redir")
|
||||||
|
|||||||
@@ -91,25 +91,38 @@ o.datatype = "min(1)"
|
|||||||
o.default = 1
|
o.default = 1
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
autoswitch_backup_node = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||||
autoswitch_backup_node:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
function o.write(self, section, value)
|
o.widget = "checkbox"
|
||||||
local t = {}
|
o.template = appname .. "/cbi/nodes_multiselect"
|
||||||
local t2 = {}
|
local keylist = {}
|
||||||
if type(value) == "table" then
|
local vallist = {}
|
||||||
local x
|
local grouplist = {}
|
||||||
for _, x in ipairs(value) do
|
for i, v in pairs(nodes_table) do
|
||||||
if x and #x > 0 then
|
keylist[i] = v.id
|
||||||
if not t2[x] then
|
vallist[i] = v.remark
|
||||||
t2[x] = x
|
grouplist[i] = v.group or ""
|
||||||
t[#t+1] = x
|
socks_node:value(v.id, v["remark"])
|
||||||
end
|
end
|
||||||
end
|
o.keylist = keylist
|
||||||
end
|
o.vallist = vallist
|
||||||
|
o.group = grouplist
|
||||||
|
-- 读取旧 DynamicList
|
||||||
|
function o.cfgvalue(self, section)
|
||||||
|
local val = m.uci:get_list(appname, section, "autoswitch_backup_node")
|
||||||
|
if val then
|
||||||
|
return val
|
||||||
else
|
else
|
||||||
t = { value }
|
return {}
|
||||||
end
|
end
|
||||||
return DynamicList.write(self, section, t)
|
end
|
||||||
|
-- 写入保持 DynamicList
|
||||||
|
function o.write(self, section, value)
|
||||||
|
local result = {}
|
||||||
|
for v in value:gmatch("%S+") do
|
||||||
|
result[#result + 1] = v
|
||||||
|
end
|
||||||
|
m.uci:set_list(appname, section, "autoswitch_backup_node", result)
|
||||||
end
|
end
|
||||||
|
|
||||||
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
||||||
@@ -125,12 +138,7 @@ o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
|
|||||||
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
for k, v in pairs(nodes_table) do
|
o = s:option(DummyValue, "btn", " ")
|
||||||
autoswitch_backup_node:value(v.id, v["remark"])
|
|
||||||
socks_node:value(v.id, v["remark"])
|
|
||||||
end
|
|
||||||
|
|
||||||
o = s:option(DummyValue, "btn", " ")
|
|
||||||
o.template = appname .. "/socks_auto_switch/btn"
|
o.template = appname .. "/socks_auto_switch/btn"
|
||||||
o:depends("enable_autoswitch", true)
|
o:depends("enable_autoswitch", true)
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
|
|||||||
id = e[".name"],
|
id = e[".name"],
|
||||||
remark = e["remark"],
|
remark = e["remark"],
|
||||||
type = e["type"],
|
type = e["type"],
|
||||||
chain_proxy = e["chain_proxy"]
|
chain_proxy = e["chain_proxy"],
|
||||||
|
group = e["group"]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
if e.protocol == "_balancing" then
|
if e.protocol == "_balancing" then
|
||||||
@@ -98,26 +99,35 @@ m.uci:foreach(appname, "socks", function(s)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
-- 负载均衡列表
|
-- 负载均衡列表
|
||||||
o = s:option(DynamicList, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
|
o = s:option(MultiValue, _n("balancing_node"), translate("Load balancing node list"), translate("Load balancing node list, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
|
||||||
o:depends({ [_n("protocol")] = "_balancing" })
|
o:depends({ [_n("protocol")] = "_balancing" })
|
||||||
local valid_ids = {}
|
o.widget = "checkbox"
|
||||||
for k, v in pairs(nodes_table) do
|
o.template = appname .. "/cbi/nodes_multiselect"
|
||||||
o:value(v.id, v.remark)
|
local keylist = {}
|
||||||
valid_ids[v.id] = true
|
local vallist = {}
|
||||||
|
local grouplist = {}
|
||||||
|
for i, v in ipairs(nodes_table) do
|
||||||
|
keylist[i] = v.id
|
||||||
|
vallist[i] = v.remark
|
||||||
|
grouplist[i] = v.group or ""
|
||||||
end
|
end
|
||||||
-- 去重并禁止自定义非法输入
|
o.keylist = keylist
|
||||||
|
o.vallist = vallist
|
||||||
|
o.group = grouplist
|
||||||
|
-- 读取旧 DynamicList
|
||||||
|
function o.cfgvalue(self, section)
|
||||||
|
local val = m.uci:get_list(appname, section, "balancing_node")
|
||||||
|
if val then
|
||||||
|
return val
|
||||||
|
else
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 写入保持 DynamicList
|
||||||
function o.custom_write(self, section, value)
|
function o.custom_write(self, section, value)
|
||||||
local result = {}
|
local result = {}
|
||||||
if type(value) == "table" then
|
for v in value:gmatch("%S+") do
|
||||||
local seen = {}
|
result[#result + 1] = v
|
||||||
for _, v in ipairs(value) do
|
|
||||||
if v and not seen[v] and valid_ids[v] then
|
|
||||||
table.insert(result, v)
|
|
||||||
seen[v] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
result = { value }
|
|
||||||
end
|
end
|
||||||
m.uci:set_list(appname, section, "balancing_node", result)
|
m.uci:set_list(appname, section, "balancing_node", result)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
|
|||||||
id = e[".name"],
|
id = e[".name"],
|
||||||
remark = e["remark"],
|
remark = e["remark"],
|
||||||
type = e["type"],
|
type = e["type"],
|
||||||
chain_proxy = e["chain_proxy"]
|
chain_proxy = e["chain_proxy"],
|
||||||
|
group = e["group"]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
if e.protocol == "_iface" then
|
if e.protocol == "_iface" then
|
||||||
@@ -105,26 +106,35 @@ m.uci:foreach(appname, "socks", function(s)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
--[[ URLTest ]]
|
--[[ URLTest ]]
|
||||||
o = s:option(DynamicList, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
|
o = s:option(MultiValue, _n("urltest_node"), translate("URLTest node list"), translate("List of nodes to test, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
|
||||||
o:depends({ [_n("protocol")] = "_urltest" })
|
o:depends({ [_n("protocol")] = "_urltest" })
|
||||||
local valid_ids = {}
|
o.widget = "checkbox"
|
||||||
for k, v in pairs(nodes_table) do
|
o.template = appname .. "/cbi/nodes_multiselect"
|
||||||
o:value(v.id, v.remark)
|
local keylist = {}
|
||||||
valid_ids[v.id] = true
|
local vallist = {}
|
||||||
|
local grouplist = {}
|
||||||
|
for i, v in ipairs(nodes_table) do
|
||||||
|
keylist[i] = v.id
|
||||||
|
vallist[i] = v.remark
|
||||||
|
grouplist[i] = v.group or ""
|
||||||
end
|
end
|
||||||
-- 去重并禁止自定义非法输入
|
o.keylist = keylist
|
||||||
|
o.vallist = vallist
|
||||||
|
o.group = grouplist
|
||||||
|
-- 读取旧 DynamicList
|
||||||
|
function o.cfgvalue(self, section)
|
||||||
|
local val = m.uci:get_list(appname, section, "urltest_node")
|
||||||
|
if val then
|
||||||
|
return val
|
||||||
|
else
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 写入保持 DynamicList
|
||||||
function o.custom_write(self, section, value)
|
function o.custom_write(self, section, value)
|
||||||
local result = {}
|
local result = {}
|
||||||
if type(value) == "table" then
|
for v in value:gmatch("%S+") do
|
||||||
local seen = {}
|
result[#result + 1] = v
|
||||||
for _, v in ipairs(value) do
|
|
||||||
if v and not seen[v] and valid_ids[v] then
|
|
||||||
table.insert(result, v)
|
|
||||||
seen[v] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
result = { value }
|
|
||||||
end
|
end
|
||||||
m.uci:set_list(appname, section, "urltest_node", result)
|
m.uci:set_list(appname, section, "urltest_node", result)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ function set_apply_on_parse(map)
|
|||||||
map.on_after_apply = function(self)
|
map.on_after_apply = function(self)
|
||||||
showMsg_Redirect(self.redirect, 3000)
|
showMsg_Redirect(self.redirect, 3000)
|
||||||
end
|
end
|
||||||
end
|
map.render = function(self, ...)
|
||||||
map.render = function(self, ...)
|
getmetatable(self).__index.render(self, ...) -- 保持原渲染流程
|
||||||
getmetatable(self).__index.render(self, ...) -- 保持原渲染流程
|
optimize_cbi_ui()
|
||||||
optimize_cbi_ui()
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
<%+cbi/valueheader%>
|
||||||
|
<%
|
||||||
|
local api = require "luci.passwall.api"
|
||||||
|
local cbid = "cbid." .. self.config .. "." .. section .. "." .. self.option
|
||||||
|
|
||||||
|
-- 读取 MultiValue
|
||||||
|
local values = {}
|
||||||
|
for i, key in pairs(self.keylist) do
|
||||||
|
values[#values + 1] = {
|
||||||
|
key = key,
|
||||||
|
label = self.vallist[i] or key,
|
||||||
|
group = self.group and self.group[i] or nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 获取选中值
|
||||||
|
local selected = {}
|
||||||
|
local cval = self:cfgvalue(section)
|
||||||
|
if type(cval) == "table" then
|
||||||
|
for _, v in pairs(cval) do
|
||||||
|
selected[v] = true
|
||||||
|
end
|
||||||
|
elseif type(cval) == "string" then
|
||||||
|
for v in cval:gmatch("%S+") do
|
||||||
|
selected[v] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 按原顺序分组
|
||||||
|
local groups = {}
|
||||||
|
local group_order = {}
|
||||||
|
for _, item in ipairs(values) do
|
||||||
|
local g = item.group
|
||||||
|
if not g or g == "" then
|
||||||
|
g = api.i18n.translate("default")
|
||||||
|
end
|
||||||
|
if not groups[g] then
|
||||||
|
groups[g] = {}
|
||||||
|
table.insert(group_order, g)
|
||||||
|
end
|
||||||
|
table.insert(groups[g], item)
|
||||||
|
end
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div id="<%=cbid%>" style="display: inline-block;">
|
||||||
|
<!-- 搜索 -->
|
||||||
|
<input type="text"
|
||||||
|
id="<%=cbid%>.search"
|
||||||
|
class="node-search-input cbi-input-text"
|
||||||
|
placeholder="<%:Search nodes...%>"
|
||||||
|
oninput="filterGroups_<%=self.option%>(this.value)"
|
||||||
|
style="width:100%;padding:6px;margin-bottom:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;" />
|
||||||
|
<!-- 主容器 -->
|
||||||
|
<div style="max-height:300px; overflow:auto; margin-bottom:8px; white-space:nowrap;">
|
||||||
|
<ul class="cbi-multi" id="<%=cbid%>.node_list" style="padding:0 !important;margin:0 !important;width:100%;box-sizing:border-box;">
|
||||||
|
<% for _, gname in ipairs(group_order) do %>
|
||||||
|
<% local items = groups[gname] %>
|
||||||
|
<li class="group-block" data-group="<%=gname%>" style="list-style:none; padding:0; margin:0 0 8px 0;">
|
||||||
|
<!-- 组标题 -->
|
||||||
|
<div class="group-title"
|
||||||
|
onclick="toggleGroup_<%=self.option%>('<%=gname%>')"
|
||||||
|
style="cursor:pointer;padding:6px;background:#f0f0f0;border-radius:4px;margin-bottom:4px;display:flex;align-items:center;white-space:nowrap;">
|
||||||
|
<span id="arrow-<%=self.option%>-<%=gname%>" style="width:16px;">▼</span>
|
||||||
|
<b><%=gname%></b>
|
||||||
|
<span id="group-count-<%=self.option%>-<%=gname%>" style="margin-left:8px;color:blue;">
|
||||||
|
(0/<%=#items%>)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 组内容(可折叠)-->
|
||||||
|
<ul id="group-<%=self.option%>-<%=gname%>" style="margin:0 0 8px 16px; padding:0; list-style:none;">
|
||||||
|
|
||||||
|
<% for _, item in ipairs(items) do %>
|
||||||
|
<li data-node-name="<%=pcdata(item.label):lower()%>" style="list-style:none; padding:0; margin:0; white-space:nowrap;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="cbi-input-checkbox"
|
||||||
|
style="vertical-align: middle; margin-right:6px;"
|
||||||
|
<%= attr("id", cbid .. "." .. item.key) ..
|
||||||
|
attr("name", cbid) ..
|
||||||
|
attr("value", item.key) ..
|
||||||
|
ifattr(selected[item.key], "checked", "checked")
|
||||||
|
%> />
|
||||||
|
<label for="<%=cbid .. "." .. item.key%>"><%=pcdata(item.label)%></label>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- 控制栏 -->
|
||||||
|
<div style="margin-top:4px;display:flex;gap:4px;align-items:center;">
|
||||||
|
<input class="btn cbi-button cbi-button-edit" type="button" onclick="selectAll_<%=self.option%>(true)" value="<%:Select all%>">
|
||||||
|
<input class="btn cbi-button cbi-button-edit" type="button" onclick="selectAll_<%=self.option%>(false)" value="<%:DeSelect all%>">
|
||||||
|
<span id="count-<%=self.option%>" style="color:#666;"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%+cbi/valuefooter%>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
//<![CDATA[
|
||||||
|
(function(){
|
||||||
|
const cbid = "<%=cbid%>";
|
||||||
|
const opt = "<%=self.option%>";
|
||||||
|
const listId = cbid + ".node_list";
|
||||||
|
|
||||||
|
// 折叠组
|
||||||
|
window["toggleGroup_" + opt] = function(g){
|
||||||
|
const ul = document.getElementById("group-" + opt + "-" + g);
|
||||||
|
const arrow = document.getElementById("arrow-" + opt + "-" + g);
|
||||||
|
if (!ul) return;
|
||||||
|
// 判断是否在搜索状态
|
||||||
|
const keyword = document.getElementById(cbid + ".search").value.trim().toLowerCase();
|
||||||
|
const isSearching = keyword.length > 0;
|
||||||
|
// 搜索状态下,仅切换当前组,不处理其他组
|
||||||
|
if (isSearching) {
|
||||||
|
if (ul.style.display === "none") {
|
||||||
|
ul.style.display = "";
|
||||||
|
if (arrow) arrow.textContent = "▼";
|
||||||
|
} else {
|
||||||
|
ul.style.display = "none";
|
||||||
|
if (arrow) arrow.textContent = "►";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 非搜索模式:先折叠其他组
|
||||||
|
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
|
||||||
|
groups.forEach(group=>{
|
||||||
|
const gname = group.getAttribute("data-group");
|
||||||
|
const gul = document.getElementById("group-" + opt + "-" + gname);
|
||||||
|
const garrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||||
|
if (gname !== g) {
|
||||||
|
if (gul) gul.style.display = "none";
|
||||||
|
if (garrow) garrow.textContent = "►";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById(listId).parentNode.scrollTop = 0;
|
||||||
|
// 切换当前组
|
||||||
|
if (ul.style.display === "none") {
|
||||||
|
ul.style.display = "";
|
||||||
|
if (arrow) arrow.textContent = "▼";
|
||||||
|
} else {
|
||||||
|
ul.style.display = "none";
|
||||||
|
if (arrow) arrow.textContent = "►";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
window["filterGroups_" + opt] = function(keyword){
|
||||||
|
keyword = keyword.toLowerCase().trim();
|
||||||
|
|
||||||
|
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
|
||||||
|
|
||||||
|
groups.forEach(group=>{
|
||||||
|
const items = group.querySelectorAll("li[data-node-name]");
|
||||||
|
let matchCount = 0;
|
||||||
|
|
||||||
|
items.forEach(li=>{
|
||||||
|
const name = li.getAttribute("data-node-name");
|
||||||
|
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||||
|
li.style.display = "";
|
||||||
|
matchCount++;
|
||||||
|
} else {
|
||||||
|
li.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 搜索时自动展开所有组
|
||||||
|
const gname = group.getAttribute("data-group");
|
||||||
|
const ul = document.getElementById("group-" + opt + "-" + gname);
|
||||||
|
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||||
|
|
||||||
|
if (matchCount === 0 && keyword !== "") {
|
||||||
|
group.style.display = "none";
|
||||||
|
} else {
|
||||||
|
group.style.display = "";
|
||||||
|
if (keyword) {
|
||||||
|
ul.style.display = "";
|
||||||
|
arrow.textContent = "▼";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCount();
|
||||||
|
|
||||||
|
// 清空搜索后恢复全部折叠
|
||||||
|
if (!keyword) {
|
||||||
|
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
|
||||||
|
groups.forEach(group=>{
|
||||||
|
const gname = group.getAttribute("data-group");
|
||||||
|
const ul = document.getElementById("group-" + opt + "-" + gname);
|
||||||
|
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||||
|
if (ul) ul.style.display = "none";
|
||||||
|
if (arrow) arrow.textContent = "►";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选 / 全不选
|
||||||
|
window["selectAll_" + opt] = function(flag){
|
||||||
|
const cbs = document.querySelectorAll("[id='" + listId + "'] input[type=checkbox]");
|
||||||
|
cbs.forEach(cb=>{
|
||||||
|
if (cb.offsetParent !== null) cb.checked = flag;
|
||||||
|
});
|
||||||
|
updateCount();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计数
|
||||||
|
function updateCount(){
|
||||||
|
const cbs = document.querySelectorAll("[id='" + listId + "'] input[type=checkbox]");
|
||||||
|
let checked = 0;
|
||||||
|
cbs.forEach(cb=>{ if (cb.checked) checked++; });
|
||||||
|
// 更新总计
|
||||||
|
document.getElementById("count-" + opt).innerHTML =
|
||||||
|
"<%:Selected:%> <span style='color:red;'>" + checked + " / " + cbs.length + "</span>";
|
||||||
|
|
||||||
|
// 更新每个组
|
||||||
|
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
|
||||||
|
groups.forEach(group=>{
|
||||||
|
const gname = group.getAttribute("data-group");
|
||||||
|
const groupCbs = group.querySelectorAll("li[data-node-name] input[type=checkbox]");
|
||||||
|
let groupChecked = 0;
|
||||||
|
groupCbs.forEach(cb=>{ if(cb.checked) groupChecked++; });
|
||||||
|
const span = document.getElementById("group-count-" + opt + "-" + gname);
|
||||||
|
if(span) span.textContent = "(" + groupChecked + "/" + groupCbs.length + ")";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById(listId)?.addEventListener("change", updateCount);
|
||||||
|
|
||||||
|
// 初始化折叠所有组和计数
|
||||||
|
const initObserver = new MutationObserver(() => {
|
||||||
|
const list = document.getElementById(listId);
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
if (list.offsetParent === null) return;
|
||||||
|
|
||||||
|
if (list.dataset.initDone === "1") return;
|
||||||
|
list.dataset.initDone = "1";
|
||||||
|
|
||||||
|
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
|
||||||
|
groups.forEach(group => {
|
||||||
|
const gname = group.getAttribute("data-group");
|
||||||
|
const ul = document.getElementById("group-" + opt + "-" + gname);
|
||||||
|
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
|
||||||
|
if (ul) ul.style.display = "none";
|
||||||
|
if (arrow) arrow.textContent = "►";
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCount();
|
||||||
|
});
|
||||||
|
|
||||||
|
initObserver.observe(document.body, {
|
||||||
|
attributes: true,
|
||||||
|
subtree: true,
|
||||||
|
attributeFilter: ["style", "class"]
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
//]]>
|
||||||
|
</script>
|
||||||
@@ -388,6 +388,9 @@ msgstr "置顶"
|
|||||||
msgid "Select"
|
msgid "Select"
|
||||||
msgstr "选择"
|
msgstr "选择"
|
||||||
|
|
||||||
|
msgid "Selected:"
|
||||||
|
msgstr "已选:"
|
||||||
|
|
||||||
msgid "DeSelect"
|
msgid "DeSelect"
|
||||||
msgstr "反选"
|
msgstr "反选"
|
||||||
|
|
||||||
@@ -2016,3 +2019,6 @@ msgstr "调整节点分组"
|
|||||||
|
|
||||||
msgid "Currently using %s node"
|
msgid "Currently using %s node"
|
||||||
msgstr "当前使用的 %s 节点"
|
msgstr "当前使用的 %s 节点"
|
||||||
|
|
||||||
|
msgid "Search nodes..."
|
||||||
|
msgstr "搜索节点…"
|
||||||
|
|||||||
@@ -143,9 +143,18 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
// TODO: Add an option to customize the keep alive period
|
if options.TCPKeepAlive >= 0 {
|
||||||
dialer.KeepAlive = C.TCPKeepAliveInitial
|
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
if keepIdle == 0 {
|
||||||
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
}
|
||||||
|
keepInterval := time.Duration(options.TCPKeepAliveInterval)
|
||||||
|
if keepInterval == 0 {
|
||||||
|
keepInterval = C.TCPKeepAliveInterval
|
||||||
|
}
|
||||||
|
dialer.KeepAlive = keepIdle
|
||||||
|
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(keepIdle, keepInterval))
|
||||||
|
}
|
||||||
var udpFragment bool
|
var udpFragment bool
|
||||||
if options.UDPFragment != nil {
|
if options.UDPFragment != nil {
|
||||||
udpFragment = *options.UDPFragment
|
udpFragment = *options.UDPFragment
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package constant
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TCPKeepAliveInitial = 10 * time.Minute
|
TCPKeepAliveInitial = 5 * time.Minute
|
||||||
TCPKeepAliveInterval = 75 * time.Second
|
TCPKeepAliveInterval = 75 * time.Second
|
||||||
TCPConnectTimeout = 5 * time.Second
|
TCPConnectTimeout = 5 * time.Second
|
||||||
TCPTimeout = 15 * time.Second
|
TCPTimeout = 15 * time.Second
|
||||||
|
|||||||
@@ -5,13 +5,29 @@ icon: material/alert-decagram
|
|||||||
#### 1.13.0-alpha.28
|
#### 1.13.0-alpha.28
|
||||||
|
|
||||||
* Update quic-go to v0.57.1
|
* Update quic-go to v0.57.1
|
||||||
|
* Add `tcp_keep_alive` and `tcp_keep_alive_interval` options for dial fields **1**
|
||||||
|
* Update default TCP keep-alive initial period from 10 minutes to 5 minutes
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
Unfortunately, for non-technical reasons, we are currently unable to notarize a standalone version of the macOS client:
|
**1**:
|
||||||
because system extensions require signatures to function, we have had to temporarily halt its release.
|
|
||||||
|
|
||||||
We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
See [Dial Fields](/configuration/shared/dial/#tcp_keep_alive).
|
||||||
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).
|
|
||||||
|
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
||||||
|
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
||||||
|
|
||||||
|
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
||||||
|
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
||||||
|
|
||||||
|
#### 1.12.13
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
||||||
|
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
||||||
|
|
||||||
|
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
||||||
|
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
||||||
|
|
||||||
#### 1.12.12
|
#### 1.12.12
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-plus: [tcp_keep_alive](#tcp_keep_alive)
|
||||||
|
:material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [domain_resolver](#domain_resolver)
|
:material-plus: [domain_resolver](#domain_resolver)
|
||||||
@@ -29,8 +34,10 @@ icon: material/new-box
|
|||||||
"connect_timeout": "",
|
"connect_timeout": "",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
"tcp_multi_path": false,
|
||||||
|
"tcp_keep_alive": "",
|
||||||
|
"tcp_keep_alive_interval": "",
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
|
|
||||||
"domain_resolver": "", // or {}
|
"domain_resolver": "", // or {}
|
||||||
"network_strategy": "",
|
"network_strategy": "",
|
||||||
"network_type": [],
|
"network_type": [],
|
||||||
@@ -112,6 +119,24 @@ Enable TCP Fast Open.
|
|||||||
|
|
||||||
Enable TCP Multi Path.
|
Enable TCP Multi Path.
|
||||||
|
|
||||||
|
#### tcp_keep_alive
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
Default value changed from `10m` to `5m`.
|
||||||
|
|
||||||
|
TCP keep-alive initial period.
|
||||||
|
|
||||||
|
`5m` will be used by default.
|
||||||
|
|
||||||
|
#### tcp_keep_alive_interval
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
TCP keep-alive interval.
|
||||||
|
|
||||||
|
`75s` will be used by default.
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
Enable UDP fragmentation.
|
Enable UDP fragmentation.
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [tcp_keep_alive](#tcp_keep_alive)
|
||||||
|
:material-plus: [tcp_keep_alive_interval](#tcp_keep_alive_interval)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [domain_resolver](#domain_resolver)
|
:material-plus: [domain_resolver](#domain_resolver)
|
||||||
@@ -29,7 +34,10 @@ icon: material/new-box
|
|||||||
"connect_timeout": "",
|
"connect_timeout": "",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
"tcp_multi_path": false,
|
||||||
|
"tcp_keep_alive": "",
|
||||||
|
"tcp_keep_alive_interval": "",
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
|
|
||||||
"domain_resolver": "", // 或 {}
|
"domain_resolver": "", // 或 {}
|
||||||
"network_strategy": "",
|
"network_strategy": "",
|
||||||
"network_type": [],
|
"network_type": [],
|
||||||
@@ -109,6 +117,24 @@ icon: material/new-box
|
|||||||
|
|
||||||
启用 TCP Multi Path。
|
启用 TCP Multi Path。
|
||||||
|
|
||||||
|
#### tcp_keep_alive
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
默认值从 `10m` 更改为 `5m`。
|
||||||
|
|
||||||
|
TCP keep-alive 初始周期。
|
||||||
|
|
||||||
|
默认使用 `5m`。
|
||||||
|
|
||||||
|
#### tcp_keep_alive_interval
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
TCP keep-alive 间隔。
|
||||||
|
|
||||||
|
默认使用 `75s`。
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
|
:material-alert: [tcp_keep_alive](#tcp_keep_alive)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [netns](#netns)
|
:material-plus: [netns](#netns)
|
||||||
@@ -29,6 +33,8 @@ icon: material/new-box
|
|||||||
"netns": "",
|
"netns": "",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
"tcp_multi_path": false,
|
||||||
|
"tcp_keep_alive": "",
|
||||||
|
"tcp_keep_alive_interval": "",
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"udp_timeout": "",
|
"udp_timeout": "",
|
||||||
"detour": "",
|
"detour": "",
|
||||||
@@ -101,6 +107,22 @@ Enable TCP Fast Open.
|
|||||||
|
|
||||||
Enable TCP Multi Path.
|
Enable TCP Multi Path.
|
||||||
|
|
||||||
|
#### tcp_keep_alive
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.13.0"
|
||||||
|
|
||||||
|
Default value changed from `10m` to `5m`.
|
||||||
|
|
||||||
|
TCP keep-alive initial period.
|
||||||
|
|
||||||
|
`5m` will be used by default.
|
||||||
|
|
||||||
|
#### tcp_keep_alive_interval
|
||||||
|
|
||||||
|
TCP keep-alive interval.
|
||||||
|
|
||||||
|
`75s` will be used by default.
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
Enable UDP fragmentation.
|
Enable UDP fragmentation.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
|
:material-alert: [tcp_keep_alive](#tcp_keep_alive)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [netns](#netns)
|
:material-plus: [netns](#netns)
|
||||||
@@ -29,6 +33,8 @@ icon: material/new-box
|
|||||||
"netns": "",
|
"netns": "",
|
||||||
"tcp_fast_open": false,
|
"tcp_fast_open": false,
|
||||||
"tcp_multi_path": false,
|
"tcp_multi_path": false,
|
||||||
|
"tcp_keep_alive": "",
|
||||||
|
"tcp_keep_alive_interval": "",
|
||||||
"udp_fragment": false,
|
"udp_fragment": false,
|
||||||
"udp_timeout": "",
|
"udp_timeout": "",
|
||||||
"detour": "",
|
"detour": "",
|
||||||
@@ -101,6 +107,22 @@ icon: material/new-box
|
|||||||
|
|
||||||
启用 TCP Multi Path。
|
启用 TCP Multi Path。
|
||||||
|
|
||||||
|
#### tcp_keep_alive
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.13.0 起"
|
||||||
|
|
||||||
|
默认值从 `10m` 更改为 `5m`。
|
||||||
|
|
||||||
|
TCP keep-alive 初始周期。
|
||||||
|
|
||||||
|
默认使用 `5m`。
|
||||||
|
|
||||||
|
#### tcp_keep_alive_interval
|
||||||
|
|
||||||
|
TCP keep-alive 间隔。
|
||||||
|
|
||||||
|
默认使用 `75s`。
|
||||||
|
|
||||||
#### udp_fragment
|
#### udp_fragment
|
||||||
|
|
||||||
启用 UDP 分段。
|
启用 UDP 分段。
|
||||||
|
|||||||
@@ -65,24 +65,26 @@ type DialerOptionsWrapper interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DialerOptions struct {
|
type DialerOptions struct {
|
||||||
Detour string `json:"detour,omitempty"`
|
Detour string `json:"detour,omitempty"`
|
||||||
BindInterface string `json:"bind_interface,omitempty"`
|
BindInterface string `json:"bind_interface,omitempty"`
|
||||||
Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"`
|
Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"`
|
||||||
Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"`
|
Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"`
|
||||||
ProtectPath string `json:"protect_path,omitempty"`
|
ProtectPath string `json:"protect_path,omitempty"`
|
||||||
RoutingMark FwMark `json:"routing_mark,omitempty"`
|
RoutingMark FwMark `json:"routing_mark,omitempty"`
|
||||||
ReuseAddr bool `json:"reuse_addr,omitempty"`
|
ReuseAddr bool `json:"reuse_addr,omitempty"`
|
||||||
NetNs string `json:"netns,omitempty"`
|
NetNs string `json:"netns,omitempty"`
|
||||||
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
|
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
|
||||||
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
|
||||||
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
|
||||||
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"`
|
||||||
UDPFragmentDefault bool `json:"-"`
|
TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"`
|
||||||
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
UDPFragment *bool `json:"udp_fragment,omitempty"`
|
||||||
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
UDPFragmentDefault bool `json:"-"`
|
||||||
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
DomainResolver *DomainResolveOptions `json:"domain_resolver,omitempty"`
|
||||||
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
|
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
||||||
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"`
|
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
|
||||||
|
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
|
||||||
|
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"`
|
||||||
|
|
||||||
// Deprecated: migrated to domain resolver
|
// Deprecated: migrated to domain resolver
|
||||||
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ function index()
|
|||||||
entry({"admin", "services", appname, "reassign_group"}, call("reassign_group")).leaf = true
|
entry({"admin", "services", appname, "reassign_group"}, call("reassign_group")).leaf = true
|
||||||
entry({"admin", "services", appname, "get_node"}, call("get_node")).leaf = true
|
entry({"admin", "services", appname, "get_node"}, call("get_node")).leaf = true
|
||||||
entry({"admin", "services", appname, "save_node_order"}, call("save_node_order")).leaf = true
|
entry({"admin", "services", appname, "save_node_order"}, call("save_node_order")).leaf = true
|
||||||
|
entry({"admin", "services", appname, "save_node_list_opt"}, call("save_node_list_opt")).leaf = true
|
||||||
entry({"admin", "services", appname, "update_rules"}, call("update_rules")).leaf = true
|
entry({"admin", "services", appname, "update_rules"}, call("update_rules")).leaf = true
|
||||||
entry({"admin", "services", appname, "subscribe_del_node"}, call("subscribe_del_node")).leaf = true
|
entry({"admin", "services", appname, "subscribe_del_node"}, call("subscribe_del_node")).leaf = true
|
||||||
entry({"admin", "services", appname, "subscribe_del_all"}, call("subscribe_del_all")).leaf = true
|
entry({"admin", "services", appname, "subscribe_del_all"}, call("subscribe_del_all")).leaf = true
|
||||||
@@ -655,6 +656,15 @@ function reassign_group()
|
|||||||
http_write_json({ status = "ok" })
|
http_write_json({ status = "ok" })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function save_node_list_opt()
|
||||||
|
local option = http.formvalue("option") or ""
|
||||||
|
local value = http.formvalue("value") or ""
|
||||||
|
if option ~= "" then
|
||||||
|
api.sh_uci_set(appname, "@global_other[0]", option, value, true)
|
||||||
|
end
|
||||||
|
http_write_json({ status = "ok" })
|
||||||
|
end
|
||||||
|
|
||||||
function update_rules()
|
function update_rules()
|
||||||
local update = http.formvalue("update")
|
local update = http.formvalue("update")
|
||||||
luci.sys.call("lua /usr/share/passwall/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &")
|
luci.sys.call("lua /usr/share/passwall/rule_update.lua log '" .. update .. "' > /dev/null 2>&1 &")
|
||||||
|
|||||||
@@ -160,6 +160,11 @@ table td, .table .td {
|
|||||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* hide save button */
|
||||||
|
.cbi-page-actions {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<% if api.is_js_luci() then -%>
|
<% if api.is_js_luci() then -%>
|
||||||
@@ -215,7 +220,7 @@ table td, .table .td {
|
|||||||
//<![CDATA[
|
//<![CDATA[
|
||||||
let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>"
|
let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>"
|
||||||
let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>"
|
let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>"
|
||||||
|
|
||||||
var node_list = [];
|
var node_list = [];
|
||||||
|
|
||||||
var ajax = {
|
var ajax = {
|
||||||
@@ -552,7 +557,7 @@ table td, .table .td {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ping_node(cbi_id, dom, type) {
|
function ping_node(cbi_id, dom, type) {
|
||||||
var full = get_address_full(cbi_id);
|
var full = get_address_full(cbi_id);
|
||||||
if ((type == "icmp" && full.address != "" ) || (type == "tcping" && full.address != "" && full.port != "")) {
|
if ((type == "icmp" && full.address != "" ) || (type == "tcping" && full.address != "" && full.port != "")) {
|
||||||
@@ -862,10 +867,10 @@ table td, .table .td {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
XHR.get('<%=api.url("get_node")%>', null,
|
function loadNodeList() {
|
||||||
function(x, result) {
|
XHR.get('<%=api.url("get_node")%>', null, function(x, result) {
|
||||||
var node_list = result
|
var node_list = result
|
||||||
|
|
||||||
var group_nodes = {}
|
var group_nodes = {}
|
||||||
for (let i = 0; i < node_list.length; i++) {
|
for (let i = 0; i < node_list.length; i++) {
|
||||||
let _node = node_list[i]
|
let _node = node_list[i]
|
||||||
@@ -877,7 +882,7 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
group_nodes[_node.group].push(_node)
|
group_nodes[_node.group].push(_node)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tab_ul_html = '<ul class="cbi-tabmenu">'
|
var tab_ul_html = '<ul class="cbi-tabmenu">'
|
||||||
var tab_ul_li_html = ''
|
var tab_ul_li_html = ''
|
||||||
var tab_content_html = '<fieldset class="cbi-section-node cbi-section-node-tabbed" id="cbi-passwall-nodes">'
|
var tab_content_html = '<fieldset class="cbi-section-node cbi-section-node-tabbed" id="cbi-passwall-nodes">'
|
||||||
@@ -926,7 +931,7 @@ table td, .table .td {
|
|||||||
innerHTML = innerHTML.split("{{remarks_val}}").join(o["remarks"]);
|
innerHTML = innerHTML.split("{{remarks_val}}").join(o["remarks"]);
|
||||||
innerHTML = innerHTML.split("{{address_val}}").join(o["address"] || "");
|
innerHTML = innerHTML.split("{{address_val}}").join(o["address"] || "");
|
||||||
innerHTML = innerHTML.split("{{port_val}}").join(o["port"] || "");
|
innerHTML = innerHTML.split("{{port_val}}").join(o["port"] || "");
|
||||||
|
|
||||||
node_tr_html += innerHTML
|
node_tr_html += innerHTML
|
||||||
}
|
}
|
||||||
_html = _html.split("{{node-tr}}").join(node_tr_html);
|
_html = _html.split("{{node-tr}}").join(node_tr_html);
|
||||||
@@ -937,7 +942,7 @@ table td, .table .td {
|
|||||||
if (group === "default") {
|
if (group === "default") {
|
||||||
group_name = "<%:default%>"
|
group_name = "<%:default%>"
|
||||||
}
|
}
|
||||||
|
|
||||||
tab_ul_li_html +=
|
tab_ul_li_html +=
|
||||||
'<li group_name="' + group + '" id="tab.passwall.nodes.' + group + '" class="cbi-tab">' +
|
'<li group_name="' + group + '" id="tab.passwall.nodes.' + group + '" class="cbi-tab">' +
|
||||||
'<a onclick="this.blur(); return cbi_t_switch(\'passwall.nodes\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.passwall.nodes=' + group + '">' + group_name + " | " + "<font style='color: red'>" + group_nodes[group].length + '</font></a>' +
|
'<a onclick="this.blur(); return cbi_t_switch(\'passwall.nodes\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.passwall.nodes=' + group + '">' + group_name + " | " + "<font style='color: red'>" + group_nodes[group].length + '</font></a>' +
|
||||||
@@ -947,17 +952,17 @@ table td, .table .td {
|
|||||||
'' + table_html +
|
'' + table_html +
|
||||||
'</div>'
|
'</div>'
|
||||||
}
|
}
|
||||||
|
|
||||||
tab_ul_html += tab_ul_li_html + '</ul>'
|
tab_ul_html += tab_ul_li_html + '</ul>'
|
||||||
tab_content_html += '</fieldset>'
|
tab_content_html += '</fieldset>'
|
||||||
var tab_html = tab_ul_html + tab_content_html
|
var tab_html = tab_ul_html + tab_content_html
|
||||||
|
|
||||||
document.getElementById("node_list").innerHTML = tab_html
|
document.getElementById("node_list").innerHTML = tab_html
|
||||||
|
|
||||||
for (let group in group_nodes) {
|
for (let group in group_nodes) {
|
||||||
cbi_t_add("passwall.nodes", group)
|
cbi_t_add("passwall.nodes", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (default_group) {
|
if (default_group) {
|
||||||
cbi_t_switch("passwall.nodes", default_group)
|
cbi_t_switch("passwall.nodes", default_group)
|
||||||
}
|
}
|
||||||
@@ -983,12 +988,60 @@ table td, .table .td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get_now_use_node();
|
get_now_use_node();
|
||||||
|
|
||||||
pingAllNodes();
|
pingAllNodes();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNodeList();
|
||||||
|
|
||||||
|
//Node list option saving logic
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
function waitForElement(selector, callback) {
|
||||||
|
const el = document.querySelector(selector);
|
||||||
|
if (el) return callback(el);
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const el = document.querySelector(selector);
|
||||||
|
if (el) {
|
||||||
|
observer.disconnect();
|
||||||
|
callback(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
function onChange(option, value) {
|
||||||
|
XHR.get('<%=api.url("save_node_list_opt")%>', {
|
||||||
|
option: option,
|
||||||
|
value: value
|
||||||
|
}, function(x) {
|
||||||
|
if (x && x.status == 200) {
|
||||||
|
document.getElementById("node_list").innerHTML = "";
|
||||||
|
loadNodeList();
|
||||||
|
} else {
|
||||||
|
alert("<%:Error%>");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForElement('input[type="checkbox"][name*="passwall"][name*="show_node_info"]', function(el) {
|
||||||
|
el.addEventListener("change", () => {
|
||||||
|
el.blur();
|
||||||
|
show_node_info = el.checked ? "1" : "0";
|
||||||
|
onChange("show_node_info", show_node_info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
waitForElement('select[name*="passwall"][name*="auto_detection_time"]', function(el) {
|
||||||
|
el.addEventListener("change", () => {
|
||||||
|
el.blur();
|
||||||
|
auto_detection_time = el.value;
|
||||||
|
onChange("auto_detection_time", auto_detection_time);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
//]]>
|
//]]>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ define Download/geoip
|
|||||||
HASH:=6878dbacfb1fcb1ee022f63ed6934bcefc95a3c4ba10c88f1131fb88dbf7c337
|
HASH:=6878dbacfb1fcb1ee022f63ed6934bcefc95a3c4ba10c88f1131fb88dbf7c337
|
||||||
endef
|
endef
|
||||||
|
|
||||||
GEOSITE_VER:=20251205081953
|
GEOSITE_VER:=20251206075552
|
||||||
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
||||||
define Download/geosite
|
define Download/geosite
|
||||||
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
||||||
URL_FILE:=dlc.dat
|
URL_FILE:=dlc.dat
|
||||||
FILE:=$(GEOSITE_FILE)
|
FILE:=$(GEOSITE_FILE)
|
||||||
HASH:=c99135dc54376b37185fd27a814435ab923f37ca6e6a784169cf743347c94beb
|
HASH:=f1276502c556709de5cc6c7581cb2e9369721c37dc6142aacf5771f7c60106f6
|
||||||
endef
|
endef
|
||||||
|
|
||||||
GEOSITE_IRAN_VER:=202512010051
|
GEOSITE_IRAN_VER:=202512010051
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
A V2Ray client for Android, support [Xray core](https://github.com/XTLS/Xray-core) and [v2fly core](https://github.com/v2fly/v2ray-core)
|
||||||
|
|
||||||
[](https://developer.android.com/about/versions/lollipop)
|
[](https://developer.android.com/about/versions/lollipop)
|
||||||
[](https://kotlinlang.org)
|
[](https://kotlinlang.org)
|
||||||
[](https://github.com/2dust/v2rayNG/commits/master)
|
[](https://github.com/2dust/v2rayNG/commits/master)
|
||||||
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
[](https://www.codefactor.io/repository/github/2dust/v2rayng)
|
||||||
[](https://github.com/2dust/v2rayNG/releases)
|
[](https://github.com/2dust/v2rayNG/releases)
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ object AppConfig {
|
|||||||
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
const val TG_CHANNEL_URL = "https://t.me/github_2dust"
|
||||||
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
const val DELAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
const val DELAY_TEST_URL2 = "https://www.google.com/generate_204"
|
||||||
const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
// const val IP_API_URL = "https://speed.cloudflare.com/meta"
|
||||||
|
const val IP_API_URL = "https://api.ip.sb/geoip"
|
||||||
|
|
||||||
/** DNS server addresses. */
|
/** DNS server addresses. */
|
||||||
const val DNS_PROXY = "1.1.1.1"
|
const val DNS_PROXY = "1.1.1.1"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.12.3"
|
agp = "8.13.1"
|
||||||
desugarJdkLibs = "2.1.5"
|
desugarJdkLibs = "2.1.5"
|
||||||
gradleLicensePlugin = "0.9.8"
|
gradleLicensePlugin = "0.9.8"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.21"
|
||||||
coreKtx = "1.16.0"
|
coreKtx = "1.16.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Thu Nov 14 12:42:51 BDT 2024
|
#Thu Nov 14 12:42:51 BDT 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -8,12 +8,10 @@ from ..utils import (
|
|||||||
ExtractorError,
|
ExtractorError,
|
||||||
determine_ext,
|
determine_ext,
|
||||||
filter_dict,
|
filter_dict,
|
||||||
get_first,
|
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
update_url,
|
update_url,
|
||||||
url_or_none,
|
url_or_none,
|
||||||
variadic,
|
|
||||||
)
|
)
|
||||||
from ..utils.traversal import traverse_obj
|
from ..utils.traversal import traverse_obj
|
||||||
|
|
||||||
@@ -51,7 +49,7 @@ class LoomIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
# m3u8 raw-url, mp4 transcoded-url, cdn url == raw-url, vtt sub and json subs
|
# m3u8 raw-url, mp4 transcoded-url, cdn url == raw-url, vtt sub and json subs
|
||||||
'url': 'https://www.loom.com/share/9458bcbf79784162aa62ffb8dd66201b',
|
'url': 'https://www.loom.com/share/9458bcbf79784162aa62ffb8dd66201b',
|
||||||
'md5': '51737ec002969dd28344db4d60b9cbbb',
|
'md5': '7b6bfdef8181c4ffc376e18919a4dcc2',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '9458bcbf79784162aa62ffb8dd66201b',
|
'id': '9458bcbf79784162aa62ffb8dd66201b',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -71,12 +69,13 @@ class LoomIE(InfoExtractor):
|
|||||||
'ext': 'webm',
|
'ext': 'webm',
|
||||||
'title': 'OMFG clown',
|
'title': 'OMFG clown',
|
||||||
'description': 'md5:285c5ee9d62aa087b7e3271b08796815',
|
'description': 'md5:285c5ee9d62aa087b7e3271b08796815',
|
||||||
'uploader': 'MrPumkin B',
|
'uploader': 'Brailey Bragg',
|
||||||
'upload_date': '20210924',
|
'upload_date': '20210924',
|
||||||
'timestamp': 1632519618,
|
'timestamp': 1632519618,
|
||||||
'duration': 210,
|
'duration': 210,
|
||||||
},
|
},
|
||||||
'params': {'skip_download': 'dash'},
|
'params': {'skip_download': 'dash'},
|
||||||
|
'expected_warnings': ['Failed to parse JSON'], # transcoded-url no longer available
|
||||||
}, {
|
}, {
|
||||||
# password-protected
|
# password-protected
|
||||||
'url': 'https://www.loom.com/share/50e26e8aeb7940189dff5630f95ce1f4',
|
'url': 'https://www.loom.com/share/50e26e8aeb7940189dff5630f95ce1f4',
|
||||||
@@ -91,10 +90,11 @@ class LoomIE(InfoExtractor):
|
|||||||
'duration': 35,
|
'duration': 35,
|
||||||
},
|
},
|
||||||
'params': {'videopassword': 'seniorinfants2'},
|
'params': {'videopassword': 'seniorinfants2'},
|
||||||
|
'expected_warnings': ['Failed to parse JSON'], # transcoded-url no longer available
|
||||||
}, {
|
}, {
|
||||||
# embed, transcoded-url endpoint sends empty JSON response, split video and audio HLS formats
|
# embed, transcoded-url endpoint sends empty JSON response, split video and audio HLS formats
|
||||||
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
'url': 'https://www.loom.com/embed/ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'md5': 'b321d261656848c184a94e3b93eae28d',
|
'md5': 'f983a0f02f24331738b2f43aecb05256',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
'id': 'ddcf1c1ad21f451ea7468b1e33917e4e',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
@@ -119,11 +119,12 @@ class LoomIE(InfoExtractor):
|
|||||||
'duration': 247,
|
'duration': 247,
|
||||||
'timestamp': 1676274030,
|
'timestamp': 1676274030,
|
||||||
},
|
},
|
||||||
|
'skip': '404 Not Found',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_GRAPHQL_VARIABLES = {
|
_GRAPHQL_VARIABLES = {
|
||||||
'GetVideoSource': {
|
'GetVideoSource': {
|
||||||
'acceptableMimes': ['DASH', 'M3U8', 'MP4'],
|
'acceptableMimes': ['DASH', 'M3U8', 'MP4', 'WEBM'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_GRAPHQL_QUERIES = {
|
_GRAPHQL_QUERIES = {
|
||||||
@@ -192,6 +193,12 @@ class LoomIE(InfoExtractor):
|
|||||||
id
|
id
|
||||||
nullableRawCdnUrl(acceptableMimes: $acceptableMimes, password: $password) {
|
nullableRawCdnUrl(acceptableMimes: $acceptableMimes, password: $password) {
|
||||||
url
|
url
|
||||||
|
credentials {
|
||||||
|
Policy
|
||||||
|
Signature
|
||||||
|
KeyPairId
|
||||||
|
__typename
|
||||||
|
}
|
||||||
__typename
|
__typename
|
||||||
}
|
}
|
||||||
__typename
|
__typename
|
||||||
@@ -240,9 +247,9 @@ class LoomIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}\n'''),
|
}\n'''),
|
||||||
}
|
}
|
||||||
_APOLLO_GRAPHQL_VERSION = '0a1856c'
|
_APOLLO_GRAPHQL_VERSION = '45a5bd4'
|
||||||
|
|
||||||
def _call_graphql_api(self, operations, video_id, note=None, errnote=None):
|
def _call_graphql_api(self, operation_name, video_id, note=None, errnote=None, fatal=True):
|
||||||
password = self.get_param('videopassword')
|
password = self.get_param('videopassword')
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
'https://www.loom.com/graphql', video_id, note or 'Downloading GraphQL JSON',
|
'https://www.loom.com/graphql', video_id, note or 'Downloading GraphQL JSON',
|
||||||
@@ -252,7 +259,9 @@ class LoomIE(InfoExtractor):
|
|||||||
'x-loom-request-source': f'loom_web_{self._APOLLO_GRAPHQL_VERSION}',
|
'x-loom-request-source': f'loom_web_{self._APOLLO_GRAPHQL_VERSION}',
|
||||||
'apollographql-client-name': 'web',
|
'apollographql-client-name': 'web',
|
||||||
'apollographql-client-version': self._APOLLO_GRAPHQL_VERSION,
|
'apollographql-client-version': self._APOLLO_GRAPHQL_VERSION,
|
||||||
}, data=json.dumps([{
|
'graphql-operation-name': operation_name,
|
||||||
|
'Origin': 'https://www.loom.com',
|
||||||
|
}, data=json.dumps({
|
||||||
'operationName': operation_name,
|
'operationName': operation_name,
|
||||||
'variables': {
|
'variables': {
|
||||||
'videoId': video_id,
|
'videoId': video_id,
|
||||||
@@ -260,7 +269,7 @@ class LoomIE(InfoExtractor):
|
|||||||
**self._GRAPHQL_VARIABLES.get(operation_name, {}),
|
**self._GRAPHQL_VARIABLES.get(operation_name, {}),
|
||||||
},
|
},
|
||||||
'query': self._GRAPHQL_QUERIES[operation_name],
|
'query': self._GRAPHQL_QUERIES[operation_name],
|
||||||
} for operation_name in variadic(operations)], separators=(',', ':')).encode())
|
}, separators=(',', ':')).encode(), fatal=fatal)
|
||||||
|
|
||||||
def _call_url_api(self, endpoint, video_id):
|
def _call_url_api(self, endpoint, video_id):
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
@@ -275,7 +284,7 @@ class LoomIE(InfoExtractor):
|
|||||||
}, separators=(',', ':')).encode())
|
}, separators=(',', ':')).encode())
|
||||||
return traverse_obj(response, ('url', {url_or_none}))
|
return traverse_obj(response, ('url', {url_or_none}))
|
||||||
|
|
||||||
def _extract_formats(self, video_id, metadata, gql_data):
|
def _extract_formats(self, video_id, metadata, video_data):
|
||||||
formats = []
|
formats = []
|
||||||
video_properties = traverse_obj(metadata, ('video_properties', {
|
video_properties = traverse_obj(metadata, ('video_properties', {
|
||||||
'width': ('width', {int_or_none}),
|
'width': ('width', {int_or_none}),
|
||||||
@@ -330,7 +339,7 @@ class LoomIE(InfoExtractor):
|
|||||||
transcoded_url = self._call_url_api('transcoded-url', video_id)
|
transcoded_url = self._call_url_api('transcoded-url', video_id)
|
||||||
formats.extend(get_formats(transcoded_url, 'transcoded', quality=-1)) # transcoded quality
|
formats.extend(get_formats(transcoded_url, 'transcoded', quality=-1)) # transcoded quality
|
||||||
|
|
||||||
cdn_url = get_first(gql_data, ('data', 'getVideo', 'nullableRawCdnUrl', 'url', {url_or_none}))
|
cdn_url = traverse_obj(video_data, ('data', 'getVideo', 'nullableRawCdnUrl', 'url', {url_or_none}))
|
||||||
# cdn_url is usually a dupe, but the raw-url/transcoded-url endpoints could return errors
|
# cdn_url is usually a dupe, but the raw-url/transcoded-url endpoints could return errors
|
||||||
valid_urls = [update_url(url, query=None) for url in (raw_url, transcoded_url) if url]
|
valid_urls = [update_url(url, query=None) for url in (raw_url, transcoded_url) if url]
|
||||||
if cdn_url and update_url(cdn_url, query=None) not in valid_urls:
|
if cdn_url and update_url(cdn_url, query=None) not in valid_urls:
|
||||||
@@ -338,10 +347,21 @@ class LoomIE(InfoExtractor):
|
|||||||
|
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
|
def _get_subtitles(self, video_id):
|
||||||
|
subs_data = self._call_graphql_api(
|
||||||
|
'FetchVideoTranscript', video_id, 'Downloading GraphQL subtitles JSON', fatal=False)
|
||||||
|
return filter_dict({
|
||||||
|
'en': traverse_obj(subs_data, (
|
||||||
|
'data', 'fetchVideoTranscript',
|
||||||
|
('source_url', 'captions_source_url'), {
|
||||||
|
'url': {url_or_none},
|
||||||
|
})) or None,
|
||||||
|
})
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
metadata = get_first(
|
metadata = traverse_obj(
|
||||||
self._call_graphql_api('GetVideoSSR', video_id, 'Downloading GraphQL metadata JSON'),
|
self._call_graphql_api('GetVideoSSR', video_id, 'Downloading GraphQL metadata JSON', fatal=False),
|
||||||
('data', 'getVideo', {dict})) or {}
|
('data', 'getVideo', {dict})) or {}
|
||||||
|
|
||||||
if metadata.get('__typename') == 'VideoPasswordMissingOrIncorrect':
|
if metadata.get('__typename') == 'VideoPasswordMissingOrIncorrect':
|
||||||
@@ -350,22 +370,19 @@ class LoomIE(InfoExtractor):
|
|||||||
'This video is password-protected, use the --video-password option', expected=True)
|
'This video is password-protected, use the --video-password option', expected=True)
|
||||||
raise ExtractorError('Invalid video password', expected=True)
|
raise ExtractorError('Invalid video password', expected=True)
|
||||||
|
|
||||||
gql_data = self._call_graphql_api(['FetchChapters', 'FetchVideoTranscript', 'GetVideoSource'], video_id)
|
video_data = self._call_graphql_api(
|
||||||
|
'GetVideoSource', video_id, 'Downloading GraphQL video JSON')
|
||||||
|
chapter_data = self._call_graphql_api(
|
||||||
|
'FetchChapters', video_id, 'Downloading GraphQL chapters JSON', fatal=False)
|
||||||
duration = traverse_obj(metadata, ('video_properties', 'duration', {int_or_none}))
|
duration = traverse_obj(metadata, ('video_properties', 'duration', {int_or_none}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'chapters': self._extract_chapters_from_description(
|
'chapters': self._extract_chapters_from_description(
|
||||||
get_first(gql_data, ('data', 'fetchVideoChapters', 'content', {str})), duration) or None,
|
traverse_obj(chapter_data, ('data', 'fetchVideoChapters', 'content', {str})), duration) or None,
|
||||||
'formats': self._extract_formats(video_id, metadata, gql_data),
|
'formats': self._extract_formats(video_id, metadata, video_data),
|
||||||
'subtitles': filter_dict({
|
'subtitles': self.extract_subtitles(video_id),
|
||||||
'en': traverse_obj(gql_data, (
|
|
||||||
..., 'data', 'fetchVideoTranscript',
|
|
||||||
('source_url', 'captions_source_url'), {
|
|
||||||
'url': {url_or_none},
|
|
||||||
})) or None,
|
|
||||||
}),
|
|
||||||
**traverse_obj(metadata, {
|
**traverse_obj(metadata, {
|
||||||
'title': ('name', {str}),
|
'title': ('name', {str}),
|
||||||
'description': ('description', {str}),
|
'description': ('description', {str}),
|
||||||
@@ -376,6 +393,7 @@ class LoomIE(InfoExtractor):
|
|||||||
|
|
||||||
|
|
||||||
class LoomFolderIE(InfoExtractor):
|
class LoomFolderIE(InfoExtractor):
|
||||||
|
_WORKING = False
|
||||||
IE_NAME = 'loom:folder'
|
IE_NAME = 'loom:folder'
|
||||||
_VALID_URL = r'https?://(?:www\.)?loom\.com/share/folder/(?P<id>[\da-f]{32})'
|
_VALID_URL = r'https?://(?:www\.)?loom\.com/share/folder/(?P<id>[\da-f]{32})'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
|
|||||||
Reference in New Issue
Block a user