Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,4 @@ tests/conf/PeerMgr
/symbols
__pycache__/
.cache/
.codegraph
50 changes: 43 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,23 @@ workspace/

| 文件 | 改了什么 |
|---|---|
| `common/rpc-service.c` | `check_permission_by_path` 与目录列举接入扩展点,新增 `cf_find_restricted_path` RPC |
| `common/rpc-service.c` | `check_permission_by_path` 与目录列举接入扩展点,新增 `cf_find_restricted_path` 与 `cf_fileop_*` RPC |
| `include/seafile-rpc.h` | 新 RPC 声明 |
| `server/seaf-server.c` | 新 RPC 注册 |
| `server/seafile-session.c` | 启动时 `cf_ext_init()` |
| `server/Makefile.am` | 新增源文件 |
| `fileserver/sync_api.go` | 同步前的子树校验(两处) |
| `server/repo-op.c` | 写入生命周期扩展点:19 个写入口各发 PREPARE / COMMITTED / ABORTED |
| `fileserver/sync_api.go` | 同步前的子树校验(两处)+ `sync-update` 生命周期 |
| `fileserver/fileop.go` | Go 侧写入口的生命周期接入 |
| `python/seaserv/api.py` | `is_repo_syncable` / `is_dir_downloadable` 透传 RPC |
| `python/seafile/rpcclient.py` | 新 RPC 客户端声明 |

**这 8 个是基线一次性付掉的代价,能力分支不应再增加。**
**这 10 个是基线一次性付掉的代价,能力分支不应再增加。**

后两个(`repo-op.c`、`fileop.go`)是写入生命周期扩展点带来的,理由写在
`cloudfile-docker/docs/fileop-lifecycle.md` 第五节:seam 不能放在已经登记过的
`rpc-service.c`,因为 `upload-file.c`、虚拟库合并和 `copy-mgr` 都直接调用
`seaf_repo_manager_*`,绕过 RPC 层——**终判点不能有绕行路**。

改动这份清单时,同步更新 `cloudfile-docker/BRANCHING.md`——那是同步上游时的
检查依据,失真就会漏掉冲突点。
Expand All @@ -71,12 +78,29 @@ docker 仓的 bootstrap 找不到该文件时会跳过并告警。
## 扩展点:cf-ext

```
common/cf-ext.{c,h} 扩展点本身:配置读取 + 能力注册表 + 三个分发钩子
fileserver/cf_ext.go 同步客户端网关,走 RPC 问 seaf-server
common/cf-ext.{c,h} 读侧扩展点:配置读取 + 能力注册表 + 三个分发钩子
common/cf-fileop.{c,h} 写侧扩展点:PREPARE / COMMITTED / ABORTED
common/cf-fileop-json.{c,h} 上面那个的 JSON 线格式(jansson 只出现在这里)
common/cf-fileop-test.c 门禁用的假 provider,默认关闭,**不是能力**
common/cf-path.{c,h} 路径规范化与组件匹配,两个扩展点共用同一份
fileserver/cf_ext.go 同步客户端网关,走 RPC 问 seaf-server
fileserver/cf_fileop.go Go 写入口网关,同样走 RPC 问 seaf-server
```

`cf_ext_init()` 里没有注册任何能力,所以基线上每个钩子都是透传,行为与原生 CE
完全一致。
完全一致。唯一的例外是 `cf-fileop-test.c`:它由 `[cloudfile]
fileop_test_provider_enabled` 门控,**默认关闭**,且刻意不进 `CF_ENABLE_*` 清单——
那份清单里的每一项都是运维可以合理打开的产品能力,而它是写入生命周期门禁用的
仪器,能拒绝写入、每次写入都追加文件,注册时会打一条明说"不要在生产里跑"的警告。

为什么用运行时开关而不是编译期剔除:编译期剔除意味着门禁跑的镜像不是发出去的
那个。这个项目为此付过一次代价——一份手写的 `seahub_settings.py` fixture 通过了
测试,而真正生成的文件抛 `NameError`、把整个文件的 CloudFile 配置一起丢掉,
服务看起来还正常起来了。**测发出去的那个。**

`cf-fileop.c`、`cf-path.c` 刻意只依赖 glib,因此 `tests/cf-fileop/run.sh` 不需要
完整的 seafile 构建就能跑——与 `cf-acl-resolve.c` 同一条理由。规格见
`cloudfile-docker/docs/fileop-lifecycle.md`。

**为什么用注册表而不是直接调用某个能力:**

Expand Down Expand Up @@ -106,7 +130,19 @@ fileserver/cf_ext.go 同步客户端网关,走 RPC 问 seaf-server
## 测试

基线没有能力实现,因此没有能力级测试;`tests/` 下的测试随能力分支一起走
(例如 `feature/dir-acl` 的 `tests/cf-acl/run.sh`)。
(例如 `feature/dir-acl` 的 `tests/cf-acl/run.sh`)。**例外是扩展点自己**:
`tests/cf-fileop/run.sh` 属于基线,它测的是 seam 而不是某个能力,包括
"没有 provider 时什么都不做"这条铁律——一个从不被断言的不变量迟早会被违反。

```bash
./tests/cf-fileop/run.sh
```

它还顺带做一件本机做不到的事的近似:`check-call-sites.py` 把 `repo-op.c` 里每个
`CF_FILEOP_*` 调用抽出来、把值换成对应类型的哑变量、再拿真正的 `cf-fileop.h`
编译一遍。`repo-op.c` 在 macOS 上编译不了,而拼错字段名、写错 operation、
少个逗号这类错误本来要等 CI 二十分钟才暴露。它**不**检查传的变量对不对——
`.name = parent_dir` 类型是对的,值是错的,那只能靠 review 和 E2E。

Go 部分:

Expand Down
35 changes: 9 additions & 26 deletions common/cf-acl-resolve.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string.h>

#include "cf-acl-resolve.h"
#include "cf-path.h"

CfAclRule *
cf_acl_rule_new (const char *path,
Expand Down Expand Up @@ -77,36 +78,18 @@ cf_acl_subject_key (int subject_type, const char *subject)
}

/*
* Collapse separators, force a leading slash, strip the trailing one.
* Deliberately leaves case and Unicode composition alone: Seafile paths are
* byte-sensitive, and folding them here would let two distinct directories
* share one ACL entry.
* Moved to common/cf-path.c when the write lifecycle seam needed the same
* rules: the ACL keys rules by path and the lock keys leases by path, and if
* the two normalized differently a rule on /a/b and a lock on /a/b/ would be
* about different objects. One implementation, so there is nothing to drift.
*
* Kept as a forwarder rather than renaming the call sites so the ACL's own
* tests and case set stayed untouched by a baseline refactor.
*/
char *
cf_acl_normalize_path (const char *path)
{
if (!path || *path == '\0')
return g_strdup ("/");

GString *buf = g_string_new ("");
const char *p = path;

while (*p) {
while (*p == '/')
p++;
if (!*p)
break;
const char *start = p;
while (*p && *p != '/')
p++;
g_string_append_c (buf, '/');
g_string_append_len (buf, start, p - start);
}

if (buf->len == 0)
g_string_append_c (buf, '/');

return g_string_free (buf, FALSE);
return cf_path_normalize (path);
}

GList *
Expand Down
12 changes: 12 additions & 0 deletions common/cf-ext.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include "seafile-session.h"
#include "cf-ext.h"
#include "cf-acl.h"
#include "cf-fileop-test.h"
#include "cf-lock.h"

typedef struct CfProvider {
char *name;
Expand All @@ -26,6 +28,14 @@ cf_ext_config_bool (const char *key)
return seaf_cfg_manager_get_config_boolean (seaf->cfg_mgr, "cloudfile", key);
}

char *
cf_ext_config_string (const char *key)
{
if (!seaf || !seaf->cfg_mgr)
return NULL;
return seaf_cfg_manager_get_config_string (seaf->cfg_mgr, "cloudfile", key);
}

gboolean
cf_ext_active (void)
{
Expand Down Expand Up @@ -61,6 +71,8 @@ cf_ext_init (void)
* this file is CloudFile's own, so editing it costs nothing at sync time.
*/
cf_acl_init ();
cf_fileop_test_init ();
cf_lock_init ();
}

char *
Expand Down
6 changes: 6 additions & 0 deletions common/cf-ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ void cf_ext_init (void);
*/
gboolean cf_ext_config_bool (const char *key);

/* A [cloudfile] string key, or NULL when unset. Newly allocated; caller
* frees. Same reason as the boolean above: one place that knows where
* capability configuration lives.
*/
char *cf_ext_config_string (const char *key);

/* Whether any capability has registered. Lets callers skip work entirely on a
* plain CE deployment.
*/
Expand Down
131 changes: 131 additions & 0 deletions common/cf-fileop-json.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

#include <jansson.h>
#include <string.h>

#include "cf-fileop-json.h"
#include "log.h"
#include "seafile-error.h"

/* Returns an owned copy of a string member, or NULL when absent, null or the
* empty string. Empty and absent are the same thing here: the Go side omits
* nothing, so "" is how it spells "not applicable". */
static char *
dup_string_member (json_t *obj, const char *key)
{
json_t *value = json_object_get (obj, key);
if (!value || !json_is_string (value))
return NULL;

const char *str = json_string_value (value);
if (!str || *str == '\0')
return NULL;

return g_strdup (str);
}

static GList *
dup_string_array (json_t *obj, const char *key)
{
json_t *array = json_object_get (obj, key);
if (!array || !json_is_array (array))
return NULL;

GList *list = NULL;
size_t i;
json_t *value;

json_array_foreach (array, i, value) {
if (!json_is_string (value))
continue;
list = g_list_append (list, g_strdup (json_string_value (value)));
}

return list;
}

CfFileOp *
cf_fileop_from_json (const char *json, GError **error)
{
json_error_t jerror;
json_t *obj = NULL;
CfFileOp *fop = NULL;
char *op = NULL;

if (!json) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS,
"Empty file operation");
return NULL;
}

obj = json_loadb (json, strlen (json), 0, &jerror);
if (!obj || !json_is_object (obj)) {
seaf_warning ("CloudFile: bad file operation payload: %s\n", jerror.text);
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS,
"Malformed file operation");
if (obj)
json_decref (obj);
return NULL;
}

op = dup_string_member (obj, "op");

/* Rejecting an unrecognised operation here rather than letting the
* dispatcher see it keeps the failure at the edge, where the payload is
* still available to log. Same reasoning as the dispatcher's own check:
* an operation nobody recognises must not sail past every provider. */
if (!cf_fileop_op_valid (op)) {
seaf_warning ("CloudFile: unknown file operation '%s' from fileserver.\n",
op ? op : "(null)");
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS,
"Unknown file operation");
g_free (op);
json_decref (obj);
return NULL;
}

fop = g_new0 (CfFileOp, 1);
fop->op = op;
fop->repo_id = dup_string_member (obj, "repo_id");
fop->dir = dup_string_member (obj, "dir");
fop->name = dup_string_member (obj, "name");
fop->names = dup_string_array (obj, "names");
fop->src_repo_id = dup_string_member (obj, "src_repo_id");
fop->src_dir = dup_string_member (obj, "src_dir");
fop->src_name = dup_string_member (obj, "src_name");
fop->src_names = dup_string_array (obj, "src_names");
fop->user = dup_string_member (obj, "user");
fop->client = dup_string_member (obj, "client");
fop->expect_commit_id = dup_string_member (obj, "expect_commit_id");
fop->commit_id = dup_string_member (obj, "commit_id");
fop->file_id = dup_string_member (obj, "file_id");

json_decref (obj);

return fop;
}

void
cf_fileop_json_free (CfFileOp *fop)
{
if (!fop)
return;

/* The struct declares these const because providers must not rewrite
* them; ownership is still ours for a parsed context. */
g_free ((char *)fop->op);
g_free ((char *)fop->repo_id);
g_free ((char *)fop->dir);
g_free ((char *)fop->name);
g_list_free_full (fop->names, g_free);
g_free ((char *)fop->src_repo_id);
g_free ((char *)fop->src_dir);
g_free ((char *)fop->src_name);
g_list_free_full (fop->src_names, g_free);
g_free ((char *)fop->user);
g_free ((char *)fop->client);
g_free ((char *)fop->expect_commit_id);
g_free ((char *)fop->commit_id);
g_free ((char *)fop->file_id);
g_free (fop);
}
36 changes: 36 additions & 0 deletions common/cf-fileop-json.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*
* JSON wire form of CfFileOp, for the Go fileserver.
*
* Split from cf-fileop.c so that file keeps depending on nothing but glib and
* still compiles into the standalone test binary. jansson lives only here.
*
* One string argument rather than a fixed-arity searpc signature: the context
* has thirteen optional fields and P1 adds session identity to it. Widening a
* searpc signature means touching the registration, the client stub and every
* caller; widening a JSON object means neither side has to move in lockstep,
* and an older peer simply does not send the new key.
*
* Wire format: cloudfile-docker/docs/fileop-lifecycle.md section 4.
*/

#ifndef CF_FILEOP_JSON_H
#define CF_FILEOP_JSON_H

#include <glib.h>

#include "cf-fileop.h"

/*
* Parse @json into a heap CfFileOp whose string fields are owned copies.
* Returns NULL and sets @error on malformed input or an operation outside the
* vocabulary.
*
* Free with cf_fileop_json_free().
*/
CfFileOp *cf_fileop_from_json (const char *json, GError **error);

void cf_fileop_json_free (CfFileOp *fop);

#endif /* CF_FILEOP_JSON_H */
Loading
Loading