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
7 changes: 5 additions & 2 deletions common/cf-v2-storage.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ cf_v2_store_dir (const char *native_obj_dir, const char *repo_id)
char *root = NULL, *key_dir = NULL, *obj_type = NULL, *result = NULL;
/* Preserve native offline export. Automatic-root discovery for export is
* a separate follow-up; never add a SQL prerequisite to native recovery. */
if (!cf_v2_storage_enabled () || !seaf->db)
if (!cf_v2_storage_enabled ())
return g_build_filename (native_obj_dir, repo_id, NULL);
if (!repo_id || !is_uuid_valid (repo_id) || !(root = storage_root ()))
/* A configured managed root must never revert to native storage when
* database initialization fails: reads and writes would diverge. */
if (!seaf->db || !repo_id || !is_uuid_valid (repo_id) ||
!(root = storage_root ()))
return NULL;
/* One query distinguishes unmapped native libraries from corrupt routes.
* No cache: separate C/Go processes must see the same committed binding. */
Expand Down
65 changes: 36 additions & 29 deletions common/obj-backend-fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "common.h"
#include "utils.h"
#include "obj-backend.h"
#include "cf-v2-storage.h"

#ifndef WIN32
#include <sys/types.h>
Expand All @@ -25,28 +26,24 @@ typedef struct FsPriv {
int dir_len;
} FsPriv;

static void
static int
id_to_path (FsPriv *priv, const char *obj_id, char path[],
const char *repo_id, int version)
{
char *pos = path;
char *dir;
int n;

#if defined MIGRATION || defined SEAFILE_CLIENT
if (version > 0) {
n = snprintf (path, SEAF_PATH_MAX, "%s/%s/", priv->obj_dir, repo_id);
pos += n;
}
#else
n = snprintf (path, SEAF_PATH_MAX, "%s/%s/", priv->obj_dir, repo_id);
pos += n;
/* Preserve the native version-0 layout for offline export/migration. */
if (version <= 0)
dir = g_strdup (priv->obj_dir);
else
#endif

memcpy (pos, obj_id, 2);
pos[2] = '/';
pos += 3;

memcpy (pos, obj_id + 2, 41 - 2);
dir = cf_v2_store_dir (priv->obj_dir, repo_id);
if (!dir)
return -1;
n = g_snprintf (path, SEAF_PATH_MAX, "%s/%.2s/%s", dir, obj_id, obj_id + 2);
g_free (dir);
return n >= SEAF_PATH_MAX ? -1 : 0;
}

static int
Expand All @@ -61,15 +58,17 @@ obj_backend_fs_read (ObjBackend *bend,
gsize tmp_len;
GError *error = NULL;

id_to_path (bend->priv, obj_id, path, repo_id, version);
if (id_to_path (bend->priv, obj_id, path, repo_id, version) < 0)
return -1;

/* seaf_debug ("object path: %s\n", path); */

g_file_get_contents (path, (gchar**)data, &tmp_len, &error);
if (error) {
#ifdef MIGRATION
g_clear_error (&error);
id_to_path (bend->priv, obj_id, path, repo_id, 1);
if (id_to_path (bend->priv, obj_id, path, repo_id, 1) < 0)
return -1;
g_file_get_contents (path, (gchar**)data, &tmp_len, &error);
if (error) {
seaf_debug ("[obj backend] Failed to read object %s: %s.\n",
Expand Down Expand Up @@ -293,7 +292,8 @@ obj_backend_fs_write (ObjBackend *bend,
{
char path[SEAF_PATH_MAX];

id_to_path (bend->priv, obj_id, path, repo_id, version);
if (id_to_path (bend->priv, obj_id, path, repo_id, version) < 0)
return -1;

/* GTimeVal s, e; */

Expand Down Expand Up @@ -328,7 +328,8 @@ obj_backend_fs_exists (ObjBackend *bend,
char path[SEAF_PATH_MAX];
SeafStat st;

id_to_path (bend->priv, obj_id, path, repo_id, version);
if (id_to_path (bend->priv, obj_id, path, repo_id, version) < 0)
return -1;

if (seaf_stat (path, &st) == 0)
return 1;
Expand All @@ -344,7 +345,8 @@ obj_backend_fs_delete (ObjBackend *bend,
{
char path[SEAF_PATH_MAX];

id_to_path (bend->priv, obj_id, path, repo_id, version);
if (id_to_path (bend->priv, obj_id, path, repo_id, version) < 0)
return -1;
if (g_unlink (path) < 0 && errno != ENOENT) {
seaf_warning ("Failed to delete object %s:%s: %s.\n",
repo_id, obj_id, strerror (errno));
Expand All @@ -370,11 +372,15 @@ obj_backend_fs_foreach_obj (ObjBackend *bend,
int ret = 0;

#if defined MIGRATION || defined SEAFILE_CLIENT
if (version > 0)
obj_dir = g_build_filename (priv->obj_dir, repo_id, NULL);
#else
obj_dir = g_build_filename (priv->obj_dir, repo_id, NULL);
if (version <= 0)
obj_dir = g_strdup (priv->obj_dir);
else
#endif
obj_dir = cf_v2_store_dir (priv->obj_dir, repo_id);
if (!obj_dir || strlen (obj_dir) + 130 >= SEAF_PATH_MAX) {
g_free (obj_dir);
return -1;
}
dir_len = strlen (obj_dir);

dir1 = g_dir_open (obj_dir, 0, NULL);
Expand Down Expand Up @@ -432,8 +438,9 @@ obj_backend_fs_copy (ObjBackend *bend,
char src_path[SEAF_PATH_MAX];
char dst_path[SEAF_PATH_MAX];

id_to_path (bend->priv, obj_id, src_path, src_repo_id, src_version);
id_to_path (bend->priv, obj_id, dst_path, dst_repo_id, dst_version);
if (id_to_path (bend->priv, obj_id, src_path, src_repo_id, src_version) < 0 ||
id_to_path (bend->priv, obj_id, dst_path, dst_repo_id, dst_version) < 0)
return -1;

if (g_file_test (dst_path, G_FILE_TEST_EXISTS))
return 0;
Expand All @@ -452,7 +459,7 @@ obj_backend_fs_copy (ObjBackend *bend,
}
return 0;
#else
int ret = link (src_path, dst_path);
int ret = cf_v2_storage_copy (src_path, dst_path);
if (ret < 0 && errno != EEXIST) {
seaf_warning ("Failed to link %s to %s: %s.\n",
src_path, dst_path, strerror(errno));
Expand All @@ -477,7 +484,7 @@ obj_backend_fs_remove_store (ObjBackend *bend,
GError *error = NULL;
int ret = 0;

obj_dir = g_build_filename (priv->obj_dir, store_id, NULL);
obj_dir = cf_v2_store_dir (priv->obj_dir, store_id);
if (!obj_dir)
return -1;

Expand Down
72 changes: 72 additions & 0 deletions fileserver/objstore/cf_v2_route_wiring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package objstore

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)

// Exercise the *production* object-store constructor, not a hand-made wrapper.
func TestV2ObjectStoreSelectsManagedRoute(t *testing.T) {
root, confDir := t.TempDir(), t.TempDir()
config := filepath.Join(confDir, "seafile.conf")
if err := os.WriteFile(config, []byte("[cloudfile_v2]\nauto_local_root="+root+"\n"), 0600); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(root, repoID), 0700); err != nil {
t.Fatal(err)
}
for _, kind := range []string{"commits", "fs", "blocks"} {
t.Run(kind, func(t *testing.T) {
for _, input := range []string{confDir, config} {
nativeDir := t.TempDir()
backend, err := newBackend(input, nativeDir, kind)
if err != nil {
t.Fatal(err)
}
routed, ok := backend.(*v2StorageBackend)
if !ok {
t.Fatalf("missing V2 route wrapper for %s", input)
}
routed.lookup = func(string) (string, string, string, error) {
return "auto-local:" + repoID, repoID, "active", nil
}
payload := []byte("C/Go storage route contract")
if err := backend.write(repoID, objID, bytes.NewReader(payload), true); err != nil {
t.Fatal(err)
}
var actual bytes.Buffer
if err := backend.read(repoID, objID, &actual); err != nil || !bytes.Equal(actual.Bytes(), payload) {
t.Fatalf("managed route: %v", err)
}
if _, err := os.Stat(filepath.Join(nativeDir, "storage", kind, repoID)); !os.IsNotExist(err) {
t.Fatal("V2 object leaked to native path")
}
// A broken registry must never be treated as an old native library.
routed.lookup = func(string) (string, string, string, error) {
return "auto-local:invalid", repoID, "active", nil
}
if err := backend.write(repoID, objID, strings.NewReader("incorrect"), false); err == nil {
t.Fatal("invalid binding was not rejected")
}
}
})
}
}

func TestV2RootRejectsMixedLegacyStorageBackends(t *testing.T) {
root := t.TempDir()
for _, backendName := range []string{"s3", "multiple"} {
confDir := t.TempDir()
value := "[cloudfile_v2]\nauto_local_root=" + root +
"\n[block_backend]\nname=" + backendName + "\n"
if err := os.WriteFile(filepath.Join(confDir, "seafile.conf"), []byte(value), 0600); err != nil {
t.Fatal(err)
}
if _, err := newBackend(confDir, t.TempDir(), "blocks"); err == nil {
t.Fatalf("mixed %s backend must fail rather than silently diverge", backendName)
}
}
}
9 changes: 7 additions & 2 deletions fileserver/objstore/cf_v2_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,13 @@ func v2Lookup(repoID string) (string, string, string, error) {
return route.String, key.String, state.String, err
}

func newV2StorageBackend(confDir string, native *fsBackend) (storageBackend, error) {
name := filepath.Join(confDir, "seafile.conf")
func newV2StorageBackend(confPath string, native *fsBackend) (storageBackend, error) {
// Fileserver callers may pass the config directory or seafile.conf itself.
// Resolve both exactly as the existing newBackend() does.
name := confPath
if info, err := os.Stat(name); err == nil && info.IsDir() {
name = filepath.Join(name, "seafile.conf")
}
cfg, err := ini.Load(name)
if os.IsNotExist(err) {
return native, nil
Expand Down
19 changes: 17 additions & 2 deletions fileserver/objstore/objstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ func New(seafileConfPath string, seafileDataDir string, objType string) *ObjectS
return obj
}

// Route ordinary filesystem objects through the existing V2 resolver. A
// single committed RepoStorageId remains authoritative for C and Go readers.
func newRoutedFSBackend(confPath, seafileDataDir, objType string) (storageBackend, error) {
native, err := newFSBackend(seafileDataDir, objType)
if err != nil {
return nil, err
}
return newV2StorageBackend(confPath, native)
}

func newBackend(seafileConfPath, seafileDataDir, objType string) (storageBackend, error) {
configPath := seafileConfPath
info, statErr := os.Stat(configPath)
Expand All @@ -54,7 +64,7 @@ func newBackend(seafileConfPath, seafileDataDir, objType string) (storageBackend
config, err := ini.Load(configPath)
if err != nil {
if os.IsNotExist(err) {
return newFSBackend(seafileDataDir, objType)
return newRoutedFSBackend(configPath, seafileDataDir, objType)
}
return nil, err
}
Expand All @@ -69,7 +79,12 @@ func newBackend(seafileConfPath, seafileDataDir, objType string) (storageBackend
}
section, err := config.GetSection(sectionName)
if err != nil || section.Key("name").String() == "" || section.Key("name").String() == "filesystem" {
return newFSBackend(seafileDataDir, objType)
return newRoutedFSBackend(configPath, seafileDataDir, objType)
}
// Legacy multi/S3 storage cannot coexist with the V2 filesystem root:
// a mixed C/Go backend would otherwise publish objects in different places.
if config.Section("cloudfile_v2").HasKey("auto_local_root") {
return nil, fmt.Errorf("V2 auto_local_root requires filesystem object backend")
}
if section.Key("name").String() != "s3" {
if section.Key("name").String() == "multiple" {
Expand Down
40 changes: 33 additions & 7 deletions server/repo-mgr.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "seaf-db.h"
#include "seaf-utils.h"
#include "cf-storage.h"
#include "cf-v2-storage.h"

#define REAP_TOKEN_INTERVAL 300 /* 5 mins */
#define DECRYPTED_TOKEN_TTL 3600 /* 1 hour */
Expand Down Expand Up @@ -2893,6 +2894,12 @@ seaf_repo_manager_del_repo_from_trash (SeafRepoManager *mgr,
const char *repo_id,
GError **error)
{
/* Preserve the storage key for cold GC and never reuse deleted keys. */
if (cf_v2_storage_retire (repo_id) < 0) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL,
"Failed to retire library storage.");
return -1;
}
/* As long as the repo is successfully moved into GarbageRepo table,
* we consider this operation successful.
*/
Expand Down Expand Up @@ -3898,9 +3905,16 @@ create_repo_common (SeafRepoManager *mgr,
}
}

/* Default local allocation belongs before the initial commit, including
* native/CLI callers that bypass the CloudFile HTTP creation endpoint. */
if (cf_pin_default_local_storage (repo_id) < 0) {
/* Allocate the default V2 key before the first commit, for native and
* API callers alike. Keep the historical class allocator only for
* deployments without a V2 root; never mix both allocation schemas. */
if (cf_v2_storage_enabled ()) {
if (cf_v2_storage_reserve (repo_id) < 0) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL,
"Failed to reserve library storage.");
return -1;
}
} else if (cf_pin_default_local_storage (repo_id) < 0) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL,
"Failed to allocate default library storage.");
return -1;
Expand Down Expand Up @@ -3969,6 +3983,12 @@ create_repo_common (SeafRepoManager *mgr,

seaf_repo_manager_update_repo_info (mgr, repo->id, repo->head->commit_id);

if (cf_v2_storage_activate (repo_id) < 0) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL,
"Failed to activate library storage.");
goto out;
}

ret = 0;
out:
if (repo)
Expand Down Expand Up @@ -4000,10 +4020,16 @@ seaf_repo_manager_create_new_repo (SeafRepoManager *mgr,

repo_id = gen_uuid ();

/* Pin the storage class before create_repo_common writes the initial
* commit: RepoStorageId must already point at the target backend or the
* root commit lands in the default store and the repo becomes unreadable
* (the multi-storage router looks the commit up in the pinned store). */
/* The V2 default root is automatic, with no per-library class selector.
* Do not persist an old cf_library_storage_key pin alongside the V2
* registry or overwrite the reserved RepoStorageId on creation. */
if (cf_v2_storage_enabled () && storage_id && storage_id[0]) {
g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS,
"Explicit storage classes are unavailable with V2 local storage.");
g_free (repo_id);
return NULL;
}
/* Preserve the pre-existing storage-class API when no V2 root is used. */
if (storage_id && storage_id[0]) {
if (cf_set_repo_storage_id (repo_id, storage_id) < 0) {
seaf_warning ("Failed to pin repo %s to storage %s.\n",
Expand Down
4 changes: 4 additions & 0 deletions tests/local_storage/block.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ int main(int argc,char **argv) {
h=b->open_block(b,repo,1,oid,BLOCK_WRITE);g_assert_nonnull(h);char *aborted=g_strdup(h->tmp_file);
b->close_block(b,h);b->block_handle_free(b,h);g_assert_false(g_file_test(aborted,G_FILE_TEST_EXISTS));
char *data=NULL;gsize len;g_assert_true(g_file_get_contents(final,&data,&len,NULL));g_assert_cmpmem(data,len,"complete",8);
if (g_getenv("CF_TEST_V2_STORAGE_ROOT")) {
char *native=g_build_filename(argv[1],"storage","blocks",repo,"ab",oid+2,NULL);
g_assert_false(g_file_test(native,G_FILE_TEST_EXISTS));g_free(native);
}
/* Unknown shard names and both old/new staging formats stay untouched. */
char *root=g_path_get_dirname(parent),*unknown_dir=g_build_filename(root,"zz",NULL);
char *unknown=g_build_filename(unknown_dir,"do-not-delete",NULL);
Expand Down
6 changes: 5 additions & 1 deletion tests/local_storage/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ static gboolean count(const char *r,int v,const char *id,void *p) {g_assert_cmps
int main(int argc,char **argv) {
g_assert_cmpint(argc,==,2);ObjBackend *b=obj_backend_fs_new(argv[1],"fs");g_assert_nonnull(b);
g_assert_cmpint(b->write(b,repo,1,oid,"complete",8,TRUE),==,0);
char final[SEAF_PATH_MAX];id_to_path(b->priv,oid,final,repo,1);
char final[SEAF_PATH_MAX];g_assert_cmpint(id_to_path(b->priv,oid,final,repo,1),==,0);
fail_write=1;g_assert_cmpint(b->write(b,repo,1,oid,"partial",7,TRUE),==,-1);fail_write=0;
char *parent=g_path_get_dirname(final);GDir *dir=g_dir_open(parent,0,NULL);const char *name;int n=0;
while ((name=g_dir_read_name(dir))) {g_assert_false(g_str_has_prefix(name,".cf-tmp-"));n++;}g_dir_close(dir);g_assert_cmpint(n,==,1);
char *orphan=g_build_filename(parent,".cf-tmp-crashed",NULL),*old=g_strconcat(final,".XXXXXX",NULL);
g_assert_true(g_file_set_contents(orphan,"partial",-1,NULL));g_assert_true(g_file_set_contents(old,"partial",-1,NULL));
n=0;g_assert_cmpint(b->foreach_obj(b,repo,1,count,&n),==,0);g_assert_cmpint(n,==,1);
char *data=NULL;gsize len;g_assert_true(g_file_get_contents(final,&data,&len,NULL));g_assert_cmpmem(data,len,"complete",8);
if (g_getenv("CF_TEST_V2_STORAGE_ROOT")) {
char *native=g_build_filename(argv[1],"storage","fs",repo,"ab",oid+2,NULL);
g_assert_false(g_file_test(native,G_FILE_TEST_EXISTS));g_free(native);
}
/* Unknown shard names and both old/new staging formats stay untouched. */
char *root=g_path_get_dirname(parent),*unknown_dir=g_build_filename(root,"zz",NULL);
char *unknown=g_build_filename(unknown_dir,"do-not-delete",NULL);
Expand Down
Loading
Loading