fix(files): 模组包上传也逐段核对 SHA-256、长文件名能写、卡住的导出按时停掉、世界被占用时文件页说明原因并等它结束

This commit is contained in:
Lemon-miaow committed 2026-09-29 01:26:24 +08:00
1 parent 1d1549cea2
commit cb3065da1d
59 files changed
+2538 -338

No files matched your search

+11
View File
@@ -43,6 +43,17 @@ const (
// process listing on the node would show it.
const TokenEnv = "FELIS_EXPORT_TOKEN"
// The Job's PUT goes chunked, so that it can end with a trailer: DigestTrailer
// carries the SHA-256 of every byte it sent (sha-256=:<base64>:, RFC 9530),
// which felis-api checks before the last of them reaches the browser.
// LengthHeader declares the length up front when the Job knows it (one file),
// which chunked encoding cannot carry, so the browser still gets a
// Content-Length.
const (
DigestTrailer = "Content-Digest"
LengthHeader = "X-Felis-Export-Length"
)
// JobParams are the rendered inputs to an export Job. ExportJob is a pure
// function of them, so the Job shape is unit-tested without a cluster.
type JobParams struct {
+34
View File
@@ -8,8 +8,11 @@ import (
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)
const secretToken = "5ecret5ecret5ecret5ecret5ecret5ecret5ecret5ecret5ecret5ecret5ecr"
@@ -236,3 +239,34 @@ func TestStartCreatesTheJob(t *testing.T) {
}
}
}
// A Job felis-api gave up on goes with its Pods, and one already gone is no
// error: the sweep that stops it may run after the Job's own TTL took it.
func TestStopDeletesTheJobAndItsPods(t *testing.T) {
cs := fake.NewSimpleClientset()
var policy metav1.DeletionPropagation
cs.PrependReactor("delete", "jobs", func(a k8stesting.Action) (bool, runtime.Object, error) {
if o := a.(k8stesting.DeleteActionImpl).GetDeleteOptions().PropagationPolicy; o != nil {
policy = *o
}
return false, nil, nil
})
e := New(cs, Config{Image: "felis:1", BackupPVC: "felis-backups"})
name, err := e.Start(context.Background(), Request{Server: "survival", Mode: ModeWorld, ID: "0011223344556677",
TargetURL: "http://api:8081/x", Token: secretToken})
if err != nil {
t.Fatal(err)
}
if err := e.Stop(context.Background(), name); err != nil {
t.Fatalf("Stop: %v", err)
}
if _, err := cs.BatchV1().Jobs("minecraft").Get(context.Background(), name, metav1.GetOptions{}); !apierrors.IsNotFound(err) {
t.Fatalf("the Job is still there: %v", err)
}
if policy != metav1.DeletePropagationBackground {
t.Errorf("propagation = %q, want Background so its Pods go too", policy)
}
if err := e.Stop(context.Background(), name); err != nil {
t.Errorf("stopping a Job already gone: %v", err)
}
}
+13
View File
@@ -20,6 +20,7 @@ import (
"time"
"felis.lolicon.best/internal/naming"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
@@ -130,3 +131,15 @@ func (e *Exporter) Start(ctx context.Context, r Request) (string, error) {
}
return job.Name, nil
}
// Stop deletes an export Job felis-api has given up on, so a Pod that never got
// going stops holding the world. Its Pods go with it; one already gone is not
// an error.
func (e *Exporter) Stop(ctx context.Context, job string) error {
bg := metav1.DeletePropagationBackground
err := e.cs.BatchV1().Jobs(e.cfg.Namespace).Delete(ctx, job, metav1.DeleteOptions{PropagationPolicy: &bg})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("worldexport: delete export job: %w", err)
}
return nil
}