载入中...
搜索中...
未找到
EditorDiskDocumentStore.cpp
浏览该文件的文档.
2
4
5#include <algorithm>
6#include <cctype>
7#include <fstream>
8#include <iterator>
9
10#if defined(_WIN32)
11#include <windows.h>
12#endif
13
14namespace eve::editor {
15namespace {
16
17const EditorValue* member(const EditorValue& value, const char* key) {
18 const auto* object = value.getIf<EditorValue::Object>();
19 if (!object) return nullptr;
20 const auto found = object->find(key);
21 return found == object->end() ? nullptr : &found->second;
22}
23
24std::string stringMember(const EditorValue& value, const char* key) {
25 const EditorValue* field = member(value, key);
26 const auto* text = field ? field->getIf<std::string>() : nullptr;
27 return text ? *text : std::string{};
28}
29
30Revision revisionMember(const EditorValue& value, const char* key) {
31 const EditorValue* field = member(value, key);
32 const auto* integer = field ? field->getIf<std::int64_t>() : nullptr;
33 return integer && *integer >= 0 ? static_cast<Revision>(*integer) : 0;
34}
35
36Result<AutosaveDraft> draftFailure(const char* rule, std::string message) {
37 return eve::editing::failed<AutosaveDraft>(EditorStatus::Rejected, RuleId(rule), std::move(message));
38}
39
40} // namespace
41
42DiskAtomicDocumentStore::DiskAtomicDocumentStore(std::filesystem::path projectRoot) {
43 std::error_code ec;
44 root_ = std::filesystem::weakly_canonical(std::move(projectRoot), ec);
45 if (ec) root_.clear();
46}
47
48std::filesystem::path DiskAtomicDocumentStore::resolve(const std::string& resourceUri) const {
49 if (root_.empty()) return {};
50 std::string relative;
51 if (resourceUri.rfind("content://", 0) == 0)
52 relative = "Content/" + resourceUri.substr(10);
53 else if (resourceUri.rfind("autosave://", 0) == 0)
54 relative = "Saved/Autosave/" + resourceUri.substr(11);
55 else
56 return {};
57 if (relative.empty()) return {};
58
59 std::filesystem::path candidate(relative);
60 if (candidate.is_absolute()) return {};
61 for (const auto& part : candidate)
62 if (part == "..") return {};
63
64 candidate = (root_ / candidate).lexically_normal();
65 const std::filesystem::path scoped = candidate.lexically_relative(root_);
66 if (scoped.empty()) return {};
67 for (const auto& part : scoped)
68 if (part == "..") return {};
69 return candidate;
70}
71
72Result<StoredDocument> DiskAtomicDocumentStore::read(const std::string& resourceUri) const {
73 const std::filesystem::path path = resolve(resourceUri);
74 if (path.empty()) return failure(EditorStatus::Rejected, "editor.document.invalid-uri", "Rejected resource URI");
75 std::ifstream input(path, std::ios::binary);
76 if (!input)
77 return failure(EditorStatus::NotFound, "editor.document.store-not-found",
78 "Document resource is not persisted: " + resourceUri);
79 const std::string json{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
80 Result<EditorValue> parsed = editorValueFromJson(json);
81 if (!parsed.ok() || !parsed.ok())
82 return failure(EditorStatus::Failed, "editor.document.invalid-envelope", "Document JSON envelope is invalid");
83
84 const EditorValue* content = member(parsed.value(), "content");
85 const Revision revision = revisionMember(parsed.value(), "revision");
86 const auto* schema = member(parsed.value(), "schemaVersion");
87 const auto* schemaInteger = schema ? schema->getIf<std::int64_t>() : nullptr;
88 if (!content || !schemaInteger || *schemaInteger != 1)
89 return failure(EditorStatus::Failed, "editor.document.unsupported-schema",
90 "Document envelope schemaVersion must be 1");
91
93 stored.content = *content;
94 stored.revision = revision;
95 stored.contentHash = editorValueContentHash(stored.content);
96 return eve::editing::applied<StoredDocument>(std::move(stored));
97}
98
100 Revision expectedRevision,
101 const std::string& expectedContentHash,
102 const EditorValue& content) {
103 const std::filesystem::path path = resolve(resourceUri);
104 if (path.empty()) return failure(EditorStatus::Rejected, "editor.document.invalid-uri", "Rejected resource URI");
105
107 Result<StoredDocument> existing = read(resourceUri);
108 if (existing.ok())
109 current = existing.value();
110 else if (existing.code() != EditorStatus::NotFound)
111 return existing;
112 else
113 current.contentHash.clear();
114 if (current.revision != expectedRevision || current.contentHash != expectedContentHash)
115 return failure(EditorStatus::Conflict, "editor.document.disk-conflict",
116 "Document content or revision changed before save");
117
118 StoredDocument next;
119 next.content = content;
120 next.revision = current.revision + 1;
121 next.contentHash = editorValueContentHash(content);
122 EditorValue::Object envelope;
123 envelope["schemaVersion"] = EditorValue(1);
124 envelope["revision"] = EditorValue(static_cast<std::int64_t>(next.revision));
125 envelope["contentHash"] = EditorValue(next.contentHash);
126 envelope["content"] = content;
127
128 std::error_code ec;
129 std::filesystem::create_directories(path.parent_path(), ec);
130 if (ec) return failure(EditorStatus::Failed, "editor.document.create-directory", ec.message());
131 const std::filesystem::path temporary = path.string() + ".tmp." + std::to_string(++sequence_);
132 {
133 std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
134 if (!output) return failure(EditorStatus::Failed, "editor.document.temp-open", "Cannot open temp file");
135 output << editorValueToJson(EditorValue(std::move(envelope)));
136 output.flush();
137 if (!output.good()) {
138 output.close();
139 std::filesystem::remove(temporary, ec);
140 return failure(EditorStatus::Failed, "editor.document.temp-write", "Cannot flush temp file");
141 }
142 }
143 if ((beforeReplace_ && !beforeReplace_(path)) || !replace(temporary, path)) {
144 std::filesystem::remove(temporary, ec);
145 return failure(EditorStatus::Failed, "editor.document.atomic-replace", "Atomic replacement failed");
146 }
147 return eve::editing::applied<StoredDocument>(std::move(next));
148}
149
150Result<StoredDocument> DiskAtomicDocumentStore::failure(EditorStatus status, const char* rule,
151 std::string message) {
152 return eve::editing::failed<StoredDocument>(status, RuleId(rule), std::move(message));
153}
154
155bool DiskAtomicDocumentStore::replace(const std::filesystem::path& temporary,
156 const std::filesystem::path& destination) const {
157#if defined(_WIN32)
158 return MoveFileExW(temporary.c_str(), destination.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
159#else
160 std::error_code ec;
161 std::filesystem::rename(temporary, destination, ec);
162 return !ec;
163#endif
164}
165
166std::string AutosaveService::draftUri(const DocumentId& document) const {
167 std::string safeDocument = document.value();
168 std::replace_if(
169 safeDocument.begin(), safeDocument.end(),
170 [](unsigned char character) { return !std::isalnum(character) && character != '-' && character != '_'; }, '_');
171 return "autosave://" + projectId_ + "/" + safeDocument + ".draft";
172}
173
175 if (!store_ || snapshot.id.empty())
176 return eve::editing::failed<StoredDocument>(EditorStatus::Rejected, RuleId("editor.autosave.invalid"),
177 "Autosave store and document are required");
178 const std::string uri = draftUri(snapshot.id);
179 Revision revision = 0;
180 std::string hash;
181 Result<StoredDocument> existing = store_->read(uri);
182 if (existing.ok()) {
183 revision = existing.value().revision;
184 hash = existing.value().contentHash;
185 } else if (existing.code() != EditorStatus::NotFound) {
186 return existing;
187 }
189 draft["document"] = EditorValue(snapshot.id.value());
190 draft["baseDiskHash"] = EditorValue(snapshot.diskContentHash);
191 draft["schemaVersion"] = EditorValue(1);
192 draft["editRevision"] = EditorValue(static_cast<std::int64_t>(snapshot.revision.edit));
193 draft["content"] = content;
194 return store_->compareAndSwap(uri, revision, hash, EditorValue(std::move(draft)));
195}
196
198 if (!store_ || document.empty()) return draftFailure("editor.autosave.invalid", "Autosave store is required");
199 const Result<StoredDocument> stored = store_->read(draftUri(document));
200 if (!stored.ok()) return Result<AutosaveDraft>::failure(stored.status());
201 const EditorValue& root = stored.value().content;
202 const EditorValue* content = member(root, "content");
203 if (!content || revisionMember(root, "schemaVersion") != 1)
204 return draftFailure("editor.autosave.unsupported-schema", "Autosave draft schemaVersion must be 1");
205 AutosaveDraft draft;
206 draft.document = DocumentId(stringMember(root, "document"));
207 draft.baseDiskHash = stringMember(root, "baseDiskHash");
208 draft.schemaVersion = 1;
209 draft.editRevision = revisionMember(root, "editRevision");
210 draft.content = *content;
211 if (draft.document != document)
212 return draftFailure("editor.autosave.document-mismatch", "Autosave draft belongs to another document");
213 return eve::editing::applied<AutosaveDraft>(std::move(draft));
214}
215
217 return draft.document == formal.id && draft.editRevision > formal.revision.saved &&
218 draft.baseDiskHash == formal.diskContentHash;
219}
220
221} // namespace eve::editor
double value
int root
Definition AnimSmr.cpp:119
Vec3 relative
Definition AnimSmr.cpp:164
std::string output
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
std::vector< std::uint8_t > stored
Definition Evpack.cpp:171
EvpackChunkInput input
Definition Evpack.cpp:170
std::string message
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
std::uint64_t revision
std::string path
Definition PlayHost.cpp:110
std::string uri
bool found
double current
float(ui::Theme::* member)[4]
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
StatusCode code() const noexcept
Inspect the stable operation code.
Definition Result.h:276
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
bool empty() const noexcept
True when no compatibility spelling has been assigned.
Definition Identity.h:374
Deterministic owning value tree shared by authoring hosts.
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Result< AutosaveDraft > readDraft(const DocumentId &document) const
Read a draft and validate its schema.
bool shouldOfferRecovery(const AutosaveDraft &draft, const DocumentSnapshot &formal) const
True when a newer draft differs from the current formal disk base.
std::string draftUri(const DocumentId &document) const
Return the stable draft URI for one document.
Result< StoredDocument > writeDraft(const DocumentSnapshot &snapshot, const EditorValue &content)
Write one revisioned draft under autosave://project/document.draft.
Result< StoredDocument > compareAndSwap(const std::string &resourceUri, Revision expectedRevision, const std::string &expectedContentHash, const EditorValue &content) override
CAS and atomically replace one document envelope on disk.
DiskAtomicDocumentStore(std::filesystem::path projectRoot)
Create a store rooted at a project directory.
std::filesystem::path resolve(const std::string &resourceUri) const
Resolve a logical URI to a canonical project path, or an empty path when rejected.
Result< StoredDocument > read(const std::string &resourceUri) const override
Read and validate the latest atomic document envelope.
virtual Result< StoredDocument > read(const std::string &resourceUri) const =0
Read the latest value for a resource URI.
virtual Result< StoredDocument > compareAndSwap(const std::string &resourceUri, Revision expectedRevision, const std::string &expectedContentHash, const EditorValue &content)=0
Replace a value only when the expected disk revision still matches.
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::editing::DocumentId DocumentId
Definition EditorIds.h:40
eve::editing::Revision Revision
const EditorValue * field(const EditorValue &value, const char *name)
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
Persisted draft metadata used to decide whether recovery should be offered.
Immutable document metadata snapshot.
Value snapshot returned by an atomic document store.