feat(seafile): add library and directory sync scope support (#13153)

### What problem does this PR solve?

The SeaFile connector currently synchronises the entire account — every
library
visible to the authenticated user. This is impractical for users who
only need
a subset of their data indexed, especially on large SeaFile instances
with many
shared libraries.

This PR introduces granular sync scope support, allowing users to choose
between
syncing their entire account, a single library, or a specific directory
within a
library. It also adds support for SeaFile library-scoped API tokens
(`/api/v2.1/via-repo-token/` endpoints), enabling tighter access control
without
exposing account-level credentials.


### Type of change

- [ ] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
- [ ] Documentation Update
- [ ] Refactoring
- [ ] Performance Improvement
- [ ] Other (please describe):

### Test

```
from seafile_connector import SeaFileConnector
import logging
import os

logging.basicConfig(level=logging.DEBUG)

URL = os.environ.get("SEAFILE_URL", "https://seafile.example.com")
TOKEN = os.environ.get("SEAFILE_TOKEN", "")
REPO_ID = os.environ.get("SEAFILE_REPO_ID", "")
SYNC_PATH = os.environ.get("SEAFILE_SYNC_PATH", "/Documents")
REPO_TOKEN = os.environ.get("SEAFILE_REPO_TOKEN", "")

def _test_scope(scope, repo_id=None, sync_path=None):
    print(f"\n{'='*50}")
    print(f"Testing scope: {scope}")
    print(f"{'='*50}")

    creds = {"seafile_token": TOKEN} if TOKEN else {}
    if REPO_TOKEN and scope in ("library", "directory"):
        creds["repo_token"] = REPO_TOKEN

    connector = SeaFileConnector(
        seafile_url=URL,
        batch_size=5,
        sync_scope=scope,
        include_shared = False,
        repo_id=repo_id,
        sync_path=sync_path,
    )
    connector.load_credentials(creds)
    connector.validate_connector_settings()

    count = 0
    for batch in connector.load_from_state():
        for doc in batch:
            count += 1
            print(f"  [{count}] {doc.semantic_identifier} "
                  f"({doc.size_bytes} bytes, {doc.extension})")

    print(f"\n-> {scope} scope: {count} document(s) found.\n")

# 1. Account scope
if TOKEN:
    _test_scope("account")
else:
    print("\nSkipping account scope (set SEAFILE_TOKEN)")

# 2. Library scope
if REPO_ID and (TOKEN or REPO_TOKEN):
    _test_scope("library", repo_id=REPO_ID)
else:
    print("\nSkipping library scope (set SEAFILE_REPO_ID + token)")

# 3. Directory scope
if REPO_ID and SYNC_PATH and (TOKEN or REPO_TOKEN):
    _test_scope("directory", repo_id=REPO_ID, sync_path=SYNC_PATH)
else:
    print("\nSkipping directory scope (set SEAFILE_REPO_ID + SEAFILE_SYNC_PATH + token)")
```
This commit is contained in:
Yesid Cano Castro
2026-02-28 03:24:28 +01:00
committed by GitHub
parent aec2ef4232
commit d1afcc9e71
7 changed files with 700 additions and 265 deletions

View File

@@ -12,6 +12,7 @@ import { IDataSourceInfoMap } from '../interface';
import { bitbucketConstant } from './bitbucket-constant';
import { confluenceConstant } from './confluence-constant';
import { S3Constant } from './s3-constant';
import { seafileConstant } from './seafile-constant';
export enum DataSourceKey {
CONFLUENCE = 'confluence',
@@ -834,39 +835,7 @@ export const DataSourceFormFields = {
],
},
],
[DataSourceKey.SEAFILE]: [
{
label: 'SeaFile Server URL',
name: 'config.seafile_url',
type: FormFieldType.Text,
required: true,
placeholder: 'https://seafile.example.com',
tooltip: t('setting.seafileUrlTip'),
},
{
label: 'API Token',
name: 'config.credentials.seafile_token',
type: FormFieldType.Password,
required: true,
tooltip: t('setting.seafileTokenTip'),
},
{
label: 'Include Shared Libraries',
name: 'config.include_shared',
type: FormFieldType.Checkbox,
required: false,
defaultValue: true,
tooltip: t('setting.seafileIncludeSharedTip'),
},
{
label: 'Batch Size',
name: 'config.batch_size',
type: FormFieldType.Number,
required: false,
placeholder: '100',
tooltip: t('setting.seafileBatchSizeTip'),
},
],
[DataSourceKey.SEAFILE]: seafileConstant(t),
[DataSourceKey.MYSQL]: [
{
label: 'Host',
@@ -1253,10 +1222,14 @@ export const DataSourceFormDefaultValues = {
source: DataSourceKey.SEAFILE,
config: {
seafile_url: '',
include_shared: true,
sync_scope: 'account',
repo_id: '',
sync_path: '',
include_shared: true,
batch_size: 100,
credentials: {
seafile_token: '',
seafile_token: '',
repo_token: '',
},
},
},

View File

@@ -0,0 +1,210 @@
import { FilterFormField, FormFieldType } from '@/components/dynamic-form';
import { TFunction } from 'i18next';
export const seafileConstant = (t: TFunction) => [
{
label: 'SeaFile Server URL',
name: 'config.seafile_url',
type: FormFieldType.Text,
required: true,
placeholder: 'https://seafile.example.com',
tooltip: t('setting.seafileUrlTip'),
},
{
label: 'Sync Scope',
name: 'config.sync_scope',
type: FormFieldType.Segmented,
options: [
{ label: 'Entire Account', value: 'account' },
{ label: 'Single Library', value: 'library' },
{ label: 'Specific Directory', value: 'directory' },
],
tooltip: t('setting.seafileSyncScopeTip'),
},
{
name: FilterFormField + '.account-tip',
label: ' ',
type: FormFieldType.Custom,
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope ?? 'account';
return scope === 'account';
},
render: () => (
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2">
{t('setting.seafileAccountScopeTip')}
</div>
),
},
{
label: 'Account API Token',
name: 'config.credentials.seafile_token',
type: FormFieldType.Password,
required: false,
defaultValue: '',
tooltip: t('setting.seafileTokenTip'),
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope ?? 'account';
return scope === 'account';
},
customValidate: (val: string, formValues: any) => {
const scope = formValues?.config?.sync_scope ?? 'account';
if ((!val || val.trim() === '') && scope === 'account') {
return t('setting.seafileValidationAccountTokenRequired');
}
return true;
},
},
{
label: 'Include Shared Libraries',
name: 'config.include_shared',
type: FormFieldType.Checkbox,
required: false,
defaultValue: true,
tooltip: t('setting.seafileIncludeSharedTip'),
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope ?? 'account';
return scope === 'account';
},
},
{
// Contextual info panel explaining the two-token choice
name: FilterFormField + '.token-tip',
label: ' ',
type: FormFieldType.Custom,
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope;
return scope === 'library' || scope === 'directory';
},
render: () => (
<div className="text-sm text-text-secondary bg-bg-card border border-border-button rounded-md px-3 py-2 space-y-1">
<p className="font-medium text-text-primary">{t('setting.seafileTokenPanelHeading')}</p>
<ul className="list-disc list-inside space-y-0.5">
<li>
<span className="font-medium">Account API Token</span>
{' ' + t('setting.seafileTokenPanelAccountBullet')}
</li>
<li>
<span className="font-medium">Library Token</span>
{' ' + t('setting.seafileTokenPanelLibraryBullet')}
</li>
</ul>
</div>
),
},
{
label: 'Account API Token',
name: 'config.credentials.seafile_token',
type: FormFieldType.Password,
required: false,
tooltip: t('setting.seafileTokenTip'),
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope;
return scope === 'library' || scope === 'directory';
},
},
{
label: 'Library Token',
name: 'config.credentials.repo_token',
type: FormFieldType.Password,
required: false,
tooltip: t('setting.seafileRepoTokenTip'),
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope;
return scope === 'library' || scope === 'directory';
},
customValidate: (val: string, formValues: any) => {
const scope = formValues?.config?.sync_scope;
const accountToken = formValues?.config?.credentials?.seafile_token;
if (!val && !accountToken && (scope === 'library' || scope === 'directory')) {
return t('setting.seafileValidationTokenRequired');
}
return true;
},
},
{
label: 'Library ID',
name: 'config.repo_id',
type: FormFieldType.Text,
required: false,
placeholder: 'e.g. 7a9e1b3c-4d5f-6a7b-8c9d-0e1f2a3b4c5d',
tooltip: t('setting.seafileRepoIdTip'),
shouldRender: (formValues: any) => {
const scope = formValues?.config?.sync_scope;
return scope === 'library' || scope === 'directory';
},
customValidate: (val: string, formValues: any) => {
const scope = formValues?.config?.sync_scope;
if (!val && (scope === 'library' || scope === 'directory')) {
return t('setting.seafileValidationLibraryIdRequired');
}
return true;
},
},
{
label: 'Directory Path',
name: 'config.sync_path',
type: FormFieldType.Text,
required: false,
placeholder: '/Documents/Reports',
tooltip: t('setting.seafileSyncPathTip'),
shouldRender: (formValues: any) => {
return formValues?.config?.sync_scope === 'directory';
},
customValidate: (val: string, formValues: any) => {
if (!val && formValues?.config?.sync_scope === 'directory') {
return t('setting.seafileValidationDirectoryPathRequired');
}
return true;
},
},
{
label: 'Batch Size',
name: 'config.batch_size',
type: FormFieldType.Number,
required: false,
placeholder: '100',
tooltip: t('setting.seafileBatchSizeTip'),
},
{
label: 'Account API Token',
name: 'config.credentials.seafile_token',
type: FormFieldType.Password,
required: false,
hidden: true,
},
{
label: 'Library Token',
name: 'config.credentials.repo_token',
type: FormFieldType.Password,
required: false,
hidden: true,
},
{
label: 'Library ID',
name: 'config.repo_id',
type: FormFieldType.Text,
required: false,
hidden: true,
},
{
label: 'Directory Path',
name: 'config.sync_path',
type: FormFieldType.Text,
required: false,
hidden: true,
},
{
label: 'Include Shared Libraries',
name: 'config.include_shared',
type: FormFieldType.Checkbox,
required: false,
hidden: true,
},
];