Skip to content

Commit 6e2d1c1

Browse files
committed
fix(toolkit): keep source DB intact when DbMove copy fails
Prevent the prior flow from deleting source databases after logging and ignoring per-file copy errors. Preserve every source until all configured databases copy successfully. Roll back partial destinations, handle recursive database contents, and return a non-zero status on failure. Finalize symlinks only after the copy phase completes, report completion once, and cover rollback and direct retry behavior in DbMoveTest.
1 parent c2e1eea commit 6e2d1c1

4 files changed

Lines changed: 512 additions & 67 deletions

File tree

plugins/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This package contains a set of tools for TRON, the followings are the documentation for each tool.
44

5+
NOTE: All `db` tools operate directly on the database files. Before performing a database
6+
operation (archive, convert, copy, lite, mv, root), you must stop the currently running
7+
FullNode service.
8+
59
## DB Archive(Requires x86 + LevelDB)
610

711
DB archive provides the ability to reformat the manifest according to the current `database`, parameters are compatible with the previous `ArchiveManifest`.

plugins/src/main/java/common/org/tron/plugins/DbMove.java

Lines changed: 174 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
import com.typesafe.config.ConfigFactory;
55
import java.io.File;
66
import java.io.IOException;
7+
import java.io.UncheckedIOException;
78
import java.nio.file.Files;
9+
import java.nio.file.LinkOption;
810
import java.nio.file.Path;
911
import java.nio.file.Paths;
1012
import java.nio.file.StandardCopyOption;
11-
import java.util.Arrays;
13+
import java.nio.file.attribute.BasicFileAttributes;
14+
import java.util.ArrayList;
1215
import java.util.HashSet;
1316
import java.util.List;
14-
import java.util.Objects;
1517
import java.util.Set;
1618
import java.util.concurrent.Callable;
19+
import java.util.concurrent.atomic.AtomicBoolean;
1720
import java.util.stream.Collectors;
21+
import java.util.stream.Stream;
1822
import lombok.extern.slf4j.Slf4j;
1923
import me.tongfei.progressbar.ProgressBar;
2024
import org.tron.plugins.utils.FileUtils;
@@ -76,77 +80,187 @@ public Integer call() throws Exception {
7680
printNotExist();
7781
return 0;
7882
}
79-
List<Property> toBeMove = dbs.stream()
80-
.map(c -> {
81-
try {
82-
return new Property(c.getString(NAME_CONFIG_KEY),
83-
Paths.get(database.toString(), dbPath, c.getString(NAME_CONFIG_KEY)),
84-
Paths.get(c.getString(PATH_CONFIG_KEY), dbPath, c.getString(NAME_CONFIG_KEY)));
85-
} catch (IOException e) {
86-
spec.commandLine().getErr().println(e);
87-
}
88-
return null;
89-
}).filter(Objects::nonNull)
90-
.filter(p -> !p.destination.equals(p.original)).collect(Collectors.toList());
83+
// Canonical path validation lives here, once every option has its final
84+
// value; ConfigConverter is limited to database-independent checks, so
85+
// option order ('-c' before or after '-d') must not change the result.
86+
List<Property> toBeMove = new ArrayList<>();
87+
for (Config c : dbs) {
88+
try {
89+
toBeMove.add(new Property(c.getString(NAME_CONFIG_KEY),
90+
Paths.get(database.toString(), dbPath, c.getString(NAME_CONFIG_KEY)),
91+
Paths.get(c.getString(PATH_CONFIG_KEY), dbPath, c.getString(NAME_CONFIG_KEY))));
92+
} catch (IOException e) {
93+
spec.commandLine().getErr().println(e);
94+
return 2;
95+
}
96+
}
97+
try {
98+
checkNoNesting(toBeMove);
99+
} catch (IllegalArgumentException e) {
100+
spec.commandLine().getErr().println(e.getMessage());
101+
return 2;
102+
}
91103

92-
if (toBeMove.isEmpty()) {
93-
printNotExist();
94-
return 0;
104+
boolean allCopied = ProgressBar.wrap(toBeMove.stream(), "copy task")
105+
.map(this::copy).reduce(Boolean.TRUE, Boolean::logicalAnd);
106+
if (!allCopied) {
107+
cleanupDestinations(toBeMove);
108+
return 1;
95109
}
96-
toBeMove = toBeMove.stream()
97-
.filter(property -> {
98-
if (property.destination.toFile().exists()) {
99-
spec.commandLine().getOut().println(String.format("%s already exist,skip.",
100-
property.destination));
101-
return false;
102-
} else {
103-
return true;
104-
}
105-
}).collect(Collectors.toList());
106110

107-
if (toBeMove.isEmpty()) {
108-
printNotExist();
109-
return 0;
111+
boolean allMoved = ProgressBar.wrap(toBeMove.stream(), "link task")
112+
.map(this::replaceSourceWithLink).reduce(Boolean.TRUE, Boolean::logicalAnd);
113+
if (!allMoved) {
114+
return 1;
110115
}
111-
ProgressBar.wrap(toBeMove.stream(), "mv task").forEach(this::run);
112116
spec.commandLine().getOut().println("move db done.");
113-
114117
} else {
115118
printNotExist();
116119
return 0;
117120
}
118121
return 0;
119122
}
120123

121-
private void run(Property p) {
122-
if (p.destination.toFile().mkdirs()) {
123-
ProgressBar.wrap(Arrays.stream(Objects.requireNonNull(p.original.toFile().listFiles()))
124-
.filter(File::isFile).map(File::getName).parallel(), p.name).forEach(file -> {
125-
Path original = Paths.get(p.original.toString(), file);
126-
Path destination = Paths.get(p.destination.toString(), file);
127-
try {
128-
Files.copy(original, destination,
129-
StandardCopyOption.REPLACE_EXISTING);
130-
} catch (IOException e) {
131-
spec.commandLine().getErr().println(e);
132-
}
133-
});
124+
private boolean copy(Property p) {
125+
if (!p.destination.toFile().mkdirs()) {
126+
spec.commandLine().getErr().println(String.format("%s create failed.", p.destination));
127+
return false;
128+
}
129+
130+
AtomicBoolean hasError = new AtomicBoolean(false);
131+
try (Stream<Path> files = Files.walk(p.original)) {
132+
ProgressBar.wrap(files.parallel(), p.name).forEach(source -> {
133+
try {
134+
copyEntry(p, source);
135+
} catch (IOException e) {
136+
hasError.set(true);
137+
spec.commandLine().getErr().println(e);
138+
}
139+
});
140+
} catch (IOException | UncheckedIOException e) {
141+
hasError.set(true);
142+
spec.commandLine().getErr().println(e);
143+
}
144+
145+
if (hasError.get()) {
146+
spec.commandLine().getErr().println(String.format(
147+
"%s copy to %s failed, source kept.",
148+
p.original, p.destination));
149+
return false;
150+
}
151+
return true;
152+
}
153+
154+
// Classify every entry WITHOUT following links: a symlink or special file
155+
// inside the db dir has no safe copy semantics and must fail the move before
156+
// the source is deleted. Directories are created explicitly so empty (nested)
157+
// directories survive the move; an attribute read failure counts as an error
158+
// instead of silently skipping the entry.
159+
private void copyEntry(Property p, Path source) throws IOException {
160+
BasicFileAttributes attributes = Files.readAttributes(
161+
source, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
162+
Path destination = p.destination.resolve(p.original.relativize(source));
163+
if (attributes.isDirectory()) {
164+
Files.createDirectories(destination);
165+
} else if (attributes.isRegularFile()) {
166+
Files.createDirectories(destination.getParent());
167+
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
168+
} else {
169+
throw new IOException(String.format(
170+
"%s is neither a regular file nor a directory, can not be moved.", source));
171+
}
172+
}
173+
174+
private boolean replaceSourceWithLink(Property p) {
175+
try {
176+
if (!FileUtils.deleteDirNoFollowLinks(p.original.toFile())) {
177+
spec.commandLine().getErr().println(String.format(
178+
"%s delete failed and may be incomplete; the only complete copy is at %s, keep it.",
179+
p.original, p.destination));
180+
printRecoveryHint(p);
181+
return false;
182+
}
183+
Files.createSymbolicLink(p.original, p.destination);
184+
return true;
185+
} catch (IOException | RuntimeException x) {
186+
spec.commandLine().getErr().println(x);
187+
spec.commandLine().getErr().println(String.format(
188+
"%s move failed; the complete copy is at %s, keep it.",
189+
p.original, p.destination));
190+
printRecoveryHint(p);
191+
return false;
192+
}
193+
}
194+
195+
private void printRecoveryHint(Property p) {
196+
spec.commandLine().getErr().println(String.format(
197+
"To recover manually: remove %s if present, then create a symbolic link at %s"
198+
+ " pointing to %s.",
199+
p.original, p.original, p.destination));
200+
}
201+
202+
private void cleanupDestinations(List<Property> properties) {
203+
boolean allCleaned = properties.stream().map(property -> {
204+
File destination = property.destination.toFile();
205+
// Fail closed: only a confirmed-absent path (no dangling link) is done.
206+
if (Files.notExists(destination.toPath(), LinkOption.NOFOLLOW_LINKS)) {
207+
return true;
208+
}
134209
try {
135-
if (FileUtils.deleteDir(p.original.toFile())) {
136-
Files.createSymbolicLink(p.original, p.destination);
210+
if (FileUtils.deleteDirNoFollowLinks(destination)) {
211+
return true;
137212
}
138-
} catch (IOException | UnsupportedOperationException x) {
139-
spec.commandLine().getErr().println(x);
213+
} catch (RuntimeException e) {
214+
spec.commandLine().getErr().println(e);
140215
}
216+
spec.commandLine().getErr().println(String.format(
217+
"%s cleanup failed; remove the leftover copy before retrying.",
218+
property.destination));
219+
return false;
220+
}).reduce(Boolean.TRUE, Boolean::logicalAnd);
221+
222+
if (allCleaned) {
223+
spec.commandLine().getErr().println(
224+
"move db failed; all source databases were kept, please retry.");
141225
} else {
142-
spec.commandLine().getErr().println(String.format("%s create failed.", p.destination));
226+
spec.commandLine().getErr().println(
227+
"move db failed; all source databases were kept, but leftover copies remain.");
143228
}
144229
}
145230

146231
private void printNotExist() {
147232
spec.commandLine().getErr().println(NOT_FIND);
148233
}
149234

235+
// Reject any overlap in the canonical path graph before mutating anything:
236+
// a destination inside ANY source would be wiped when that source is deleted
237+
// in the link phase; overlapping sources make the parent's deletion remove
238+
// the child's fresh link; overlapping destinations interleave two copies.
239+
private static void checkNoNesting(List<Property> properties) {
240+
for (Property a : properties) {
241+
for (Property b : properties) {
242+
if (b.destination.startsWith(a.original)) {
243+
throw new IllegalArgumentException(String.format(
244+
"destination [%s] can not be inside original [%s],please check!",
245+
b.destination, a.original));
246+
}
247+
if (a == b) {
248+
continue;
249+
}
250+
if (b.original.startsWith(a.original)) {
251+
throw new IllegalArgumentException(String.format(
252+
"original [%s] can not overlap original [%s],please check!",
253+
b.original, a.original));
254+
}
255+
if (b.destination.startsWith(a.destination)) {
256+
throw new IllegalArgumentException(String.format(
257+
"destination [%s] can not overlap destination [%s],please check!",
258+
b.destination, a.destination));
259+
}
260+
}
261+
}
262+
}
263+
150264

151265
static class Property {
152266

@@ -167,7 +281,9 @@ public Property(String name, Path original, Path destination) throws IOException
167281
throw new IOException(original + " is symbolicLink!");
168282
}
169283
this.destination = destination.toFile().getCanonicalFile().toPath();
170-
if (this.destination.toFile().exists()) {
284+
// Fail closed: File.exists() follows links, so a dangling symlink at the
285+
// destination would pass as absent only to fail mkdirs on every retry.
286+
if (!Files.notExists(this.destination, LinkOption.NOFOLLOW_LINKS)) {
171287
throw new IOException(this.destination + " already exist!");
172288
}
173289
if (this.destination.equals(this.original)) {
@@ -195,25 +311,22 @@ public Config convert(String value) throws Exception {
195311
if (dbs.isEmpty()) {
196312
throw notFind;
197313
}
198-
String dbPath = config.hasPath(DB_DIRECTORY_CONFIG_KEY)
199-
? config.getString(DB_DIRECTORY_CONFIG_KEY) : DEFAULT_DB_DIRECTORY;
200-
201314
dbs = dbs.stream()
202315
.filter(c -> c.hasPath(NAME_CONFIG_KEY) && c.hasPath(PATH_CONFIG_KEY))
203316
.collect(Collectors.toList());
204317

205318
if (dbs.isEmpty()) {
206319
throw notFind;
207320
}
208-
Set<String> toBeMove = new HashSet<>();
321+
// Only database-independent checks may run at conversion time: the
322+
// static `database` option may not have its final value yet ('-c'
323+
// can be parsed before '-d'). Path validation happens in call().
324+
Set<String> names = new HashSet<>();
209325
for (Config c : dbs) {
210-
if (!toBeMove.add(new Property(c.getString(NAME_CONFIG_KEY),
211-
Paths.get(database.toString(), dbPath, c.getString(NAME_CONFIG_KEY)),
212-
Paths.get(c.getString(PATH_CONFIG_KEY), dbPath,
213-
c.getString(NAME_CONFIG_KEY))).name)) {
326+
String name = c.getString(NAME_CONFIG_KEY);
327+
if (!names.add(name)) {
214328
throw new IllegalArgumentException(
215-
"DB config has duplicate key:[" + c.getString(NAME_CONFIG_KEY)
216-
+ "],please check! ");
329+
"DB config has duplicate key:[" + name + "],please check! ");
217330
}
218331
}
219332
} else {

plugins/src/main/java/common/org/tron/plugins/utils/FileUtils.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@ public static boolean deleteDir(File dir) {
8383
return dir.delete();
8484
}
8585

86+
/**
87+
* Delete {@code dir} recursively WITHOUT following symbolic links: a symlink
88+
* is removed itself and its target is left untouched. Use for trees that may
89+
* contain links to data that must survive, e.g. database dirs already turned
90+
* into links by {@code db mv}. {@link #deleteDir(File)} keeps the legacy
91+
* follow-links semantics that DbLite/DbConvert rely on to free space.
92+
*/
93+
public static boolean deleteDirNoFollowLinks(File dir) {
94+
if (Files.isSymbolicLink(dir.toPath())) {
95+
return dir.delete();
96+
}
97+
if (dir.isDirectory()) {
98+
String[] children = dir.list();
99+
if (children == null) {
100+
return false;
101+
}
102+
for (String child : children) {
103+
if (!deleteDirNoFollowLinks(new File(dir, child))) {
104+
return false;
105+
}
106+
}
107+
}
108+
return dir.delete();
109+
}
110+
86111
public static boolean createFileIfNotExists(String filepath) {
87112
File file = new File(filepath);
88113
if (!file.exists()) {

0 commit comments

Comments
 (0)