Skip to content

Commit cc85dc5

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.
1 parent 371e6c3 commit cc85dc5

4 files changed

Lines changed: 351 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/Db.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
mixinStandardHelpOptions = true,
77
version = "db command 1.0",
88
description = "An rich command set that provides high-level operations for dbs.",
9+
header = "All `db` tools operate directly on the database files.\n"
10+
+ "Before performing a database operation,\n"
11+
+ "you must stop the currently running FullNode service.\n",
912
subcommands = {CommandLine.HelpCommand.class,
1013
DbMove.class,
1114
DbArchive.class,

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

Lines changed: 127 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,70 +80,138 @@ 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());
91-
92-
if (toBeMove.isEmpty()) {
93-
printNotExist();
94-
return 0;
83+
List<Property> toBeMove = new ArrayList<>();
84+
for (Config c : dbs) {
85+
try {
86+
toBeMove.add(new Property(c.getString(NAME_CONFIG_KEY),
87+
Paths.get(database.toString(), dbPath, c.getString(NAME_CONFIG_KEY)),
88+
Paths.get(c.getString(PATH_CONFIG_KEY), dbPath, c.getString(NAME_CONFIG_KEY))));
89+
} catch (IOException e) {
90+
spec.commandLine().getErr().println(e);
91+
return 2;
92+
}
93+
}
94+
boolean allCopied = ProgressBar.wrap(toBeMove.stream(), "copy task")
95+
.allMatch(this::copy);
96+
if (!allCopied) {
97+
cleanupDestinations(toBeMove);
98+
return 1;
9599
}
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());
106100

107-
if (toBeMove.isEmpty()) {
108-
printNotExist();
109-
return 0;
101+
boolean allMoved = ProgressBar.wrap(toBeMove.stream(), "link task")
102+
.map(this::replaceSourceWithLink).reduce(Boolean.TRUE, Boolean::logicalAnd);
103+
if (!allMoved) {
104+
return 1;
110105
}
111-
ProgressBar.wrap(toBeMove.stream(), "mv task").forEach(this::run);
112106
spec.commandLine().getOut().println("move db done.");
113-
114107
} else {
115108
printNotExist();
116109
return 0;
117110
}
118111
return 0;
119112
}
120113

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-
});
114+
private boolean copy(Property p) {
115+
if (!p.destination.toFile().mkdirs()) {
116+
spec.commandLine().getErr().println(String.format("%s create failed.", p.destination));
117+
return false;
118+
}
119+
120+
AtomicBoolean hasError = new AtomicBoolean(false);
121+
try (Stream<Path> files = Files.walk(p.original)) {
122+
ProgressBar.wrap(files.parallel(), p.name).forEach(source -> {
123+
if (hasError.get()) {
124+
return;
125+
}
126+
try {
127+
copyEntry(p, source);
128+
} catch (IOException e) {
129+
hasError.set(true);
130+
spec.commandLine().getErr().println(e);
131+
}
132+
});
133+
} catch (IOException | UncheckedIOException e) {
134+
hasError.set(true);
135+
spec.commandLine().getErr().println(e);
136+
}
137+
138+
if (hasError.get()) {
139+
spec.commandLine().getErr().println(String.format(
140+
"%s copy to %s failed, source kept.",
141+
p.original, p.destination));
142+
return false;
143+
}
144+
return true;
145+
}
146+
147+
private void copyEntry(Property p, Path source) throws IOException {
148+
BasicFileAttributes attributes = Files.readAttributes(
149+
source, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
150+
Path destination = p.destination.resolve(p.original.relativize(source));
151+
if (attributes.isDirectory()) {
152+
Files.createDirectories(destination);
153+
} else if (attributes.isRegularFile()) {
154+
Files.createDirectories(destination.getParent());
155+
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
156+
} else {
157+
throw new IOException(String.format(
158+
"%s is neither a regular file nor a directory, can not be moved.", source));
159+
}
160+
}
161+
162+
private boolean replaceSourceWithLink(Property p) {
163+
try {
164+
if (!FileUtils.deleteDir(p.original.toFile())) {
165+
spec.commandLine().getErr().println(String.format(
166+
"%s delete failed and may be incomplete; the only complete copy is at %s, keep it.",
167+
p.original, p.destination));
168+
printRecoveryHint(p);
169+
return false;
170+
}
171+
Files.createSymbolicLink(p.original, p.destination);
172+
return true;
173+
} catch (IOException | RuntimeException x) {
174+
spec.commandLine().getErr().println(x);
175+
spec.commandLine().getErr().println(String.format(
176+
"%s move failed; the complete copy is at %s, keep it.",
177+
p.original, p.destination));
178+
printRecoveryHint(p);
179+
return false;
180+
}
181+
}
182+
183+
private void printRecoveryHint(Property p) {
184+
spec.commandLine().getErr().println(String.format(
185+
"To recover manually: remove %s if present, then create a symbolic link at %s"
186+
+ " pointing to %s.",
187+
p.original, p.original, p.destination));
188+
}
189+
190+
private void cleanupDestinations(List<Property> properties) {
191+
boolean allCleaned = properties.stream().map(property -> {
192+
File destination = property.destination.toFile();
193+
if (Files.notExists(destination.toPath(), LinkOption.NOFOLLOW_LINKS)) {
194+
return true;
195+
}
134196
try {
135-
if (FileUtils.deleteDir(p.original.toFile())) {
136-
Files.createSymbolicLink(p.original, p.destination);
197+
if (FileUtils.deleteDir(destination)) {
198+
return true;
137199
}
138-
} catch (IOException | UnsupportedOperationException x) {
139-
spec.commandLine().getErr().println(x);
200+
} catch (RuntimeException e) {
201+
spec.commandLine().getErr().println(e);
140202
}
203+
spec.commandLine().getErr().println(String.format(
204+
"%s cleanup failed; remove the leftover copy before retrying.",
205+
property.destination));
206+
return false;
207+
}).reduce(Boolean.TRUE, Boolean::logicalAnd);
208+
209+
if (allCleaned) {
210+
spec.commandLine().getErr().println(
211+
"move db failed; all source databases were kept, please retry.");
141212
} else {
142-
spec.commandLine().getErr().println(String.format("%s create failed.", p.destination));
213+
spec.commandLine().getErr().println(
214+
"move db failed; all source databases were kept, but leftover copies remain.");
143215
}
144216
}
145217

@@ -167,7 +239,7 @@ public Property(String name, Path original, Path destination) throws IOException
167239
throw new IOException(original + " is symbolicLink!");
168240
}
169241
this.destination = destination.toFile().getCanonicalFile().toPath();
170-
if (this.destination.toFile().exists()) {
242+
if (!Files.notExists(this.destination, LinkOption.NOFOLLOW_LINKS)) {
171243
throw new IOException(this.destination + " already exist!");
172244
}
173245
if (this.destination.equals(this.original)) {
@@ -195,9 +267,6 @@ public Config convert(String value) throws Exception {
195267
if (dbs.isEmpty()) {
196268
throw notFind;
197269
}
198-
String dbPath = config.hasPath(DB_DIRECTORY_CONFIG_KEY)
199-
? config.getString(DB_DIRECTORY_CONFIG_KEY) : DEFAULT_DB_DIRECTORY;
200-
201270
dbs = dbs.stream()
202271
.filter(c -> c.hasPath(NAME_CONFIG_KEY) && c.hasPath(PATH_CONFIG_KEY))
203272
.collect(Collectors.toList());
@@ -207,13 +276,10 @@ public Config convert(String value) throws Exception {
207276
}
208277
Set<String> toBeMove = new HashSet<>();
209278
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)) {
279+
String name = c.getString(NAME_CONFIG_KEY);
280+
if (!toBeMove.add(name)) {
214281
throw new IllegalArgumentException(
215-
"DB config has duplicate key:[" + c.getString(NAME_CONFIG_KEY)
216-
+ "],please check! ");
282+
"DB config has duplicate key:[" + name + "],please check! ");
217283
}
218284
}
219285
} else {

0 commit comments

Comments
 (0)