Skip to content
Open
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
12 changes: 7 additions & 5 deletions DriveBackup/src/main/java/ratismal/drivebackup/UploadThread.java
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ void run_internal() {
backupBackingUp++;
for (Path folder : set.location.getPaths()) {
if (set.create) {
makeBackupFile(folder.toString(), set.formatter, Arrays.asList(set.blacklist));
makeBackupFile(folder.toString(), set.formatter, Arrays.asList(set.blacklist), set.replace);
}
}
}
Expand Down Expand Up @@ -381,11 +381,11 @@ private void pruneLocalBackups() {
* @param formatter save format configuration
* @param blackList a configured blacklist (with globs)
*/
private void makeBackupFile(String location, LocalDateTimeFormatter formatter, List<String> blackList) {
private void makeBackupFile(String location, LocalDateTimeFormatter formatter, List<String> blackList, Map<String, String> replace) {
logger.info(intl("backup-local-file-start"), "location", location);
try {
ServerUtil.setAutoSave(false);
fileUtil.makeBackup(location, formatter, blackList);
fileUtil.makeBackup(location, formatter, blackList, replace);
} catch (AbsolutePathException exception) {
logger.log(intl("backup-failed-absolute-path"));
return;
Expand Down Expand Up @@ -524,7 +524,8 @@ private void makeExternalFileBackup(ExternalFTPSource externalBackup) {
new PathBackupLocation("external-backups" + "/" + tempFolderName),
externalBackup.format,
true,
new String[0]
new String[0],
new HashMap<>()
);
backupList.add(backup);
if (ftpUploader.isErrorWhileUploading()) {
Expand Down Expand Up @@ -569,7 +570,8 @@ private void makeExternalDatabaseBackup(ExternalMySQLSource externalBackup) {
new PathBackupLocation("external-backups" + "/" + tempFolderName),
externalBackup.format,
true,
new String[0]
new String[0],
new HashMap<>()
);
backupList.add(backup);
if (mysqlUploader.isErrorWhileUploading()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -63,18 +65,21 @@ public String toString() {
public final LocalDateTimeFormatter formatter;
public final boolean create;
public final String[] blacklist;
public final Map<String, String> replace;

public BackupListEntry(
BackupLocation location,
LocalDateTimeFormatter formatter,
boolean create,
String[] blacklist
String[] blacklist,
Map<String, String> replace
) {

this.location = location;
this.formatter = formatter;
this.create = create;
this.blacklist = blacklist;
this.replace = replace;
}
}

Expand Down Expand Up @@ -134,7 +139,23 @@ public static BackupList parse(@NotNull FileConfiguration config, Logger logger)
logger.log(intl("backup-list-blacklist-invalid"), ENTRY, entryIndex);
}
}
list.add(new BackupListEntry(location, formatter, create, blacklist));
Map<String, String> replace = new LinkedHashMap<>();
if (rawListEntry.containsKey("path") && rawListEntry.containsKey("replace")) {
try {
List<Map<String, String>> replaceList = (List<Map<String, String>>) rawListEntry.get("replace");
for (Map<String, String> replaceListEntry : replaceList) {
if (replaceListEntry.containsKey("file") && replaceListEntry.containsKey("with")) {
replace.put(replaceListEntry.get("file"), replaceListEntry.get("with"));
} else {
replace.clear();
throw new IllegalArgumentException();
}
}
} catch (IllegalArgumentException | ClassCastException e) {
logger.log(intl("backup-list-replace-invalid"), ENTRY, entryIndex);
}
}
list.add(new BackupListEntry(location, formatter, create, blacklist, replace));
}
return new BackupList(list.toArray(new BackupListEntry[0]));
}
Expand Down
70 changes: 66 additions & 4 deletions DriveBackup/src/main/java/ratismal/drivebackup/util/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -71,7 +73,7 @@ public TreeMap<Long, File> getLocalBackups(String location, LocalDateTimeFormatt
* @param blacklistGlobs a list of glob patterns of files/folders to not include in the backup.
* @throws Exception
*/
public void makeBackup(@NotNull String location, LocalDateTimeFormatter formatter, List<String> blacklistGlobs) throws Exception {
public void makeBackup(@NotNull String location, LocalDateTimeFormatter formatter, List<String> blacklistGlobs, Map<String, String> replace) throws Exception {
Config config = ConfigParser.getConfig();
if (location.charAt(0) == '/') {
throw new AbsolutePathException("Location cannot start with a slash");
Expand Down Expand Up @@ -105,6 +107,41 @@ public void makeBackup(@NotNull String location, LocalDateTimeFormatter formatte
"glob-pattern", globPattern);
}
}
Map<String, String> checkedReplace = new HashMap<>();
if (Files.isDirectory(Paths.get(location))) {
Map<Path, String> fileListPaths = fileList.getList().stream()
.collect(Collectors.toMap(Paths::get, s -> s, (a, b) -> a));
replace.forEach((key, value) -> {
if (isUnsafeRelativePath(key) || isUnsafeRelativePath(value)) {
logger.info(intl("backup-list-replace-unsafe-path"), "file-path", key + " -> " + value);
return;
}
File file = new File(location + "/" + key);
File with = new File(location + "/" + value);
if (!isWithinLocation(location, with)) {
logger.info(intl("local-backup-replace-escapes-location"), "file-path", key + " -> " + value, "location", location);
return;
}
Path filePath = Paths.get(key);
Path withPath = Paths.get(value);
String matched = fileListPaths.get(filePath);
if (file.isFile() && with.isFile() && matched != null) {
if (isWithinLocation(config.backupStorage.localDirectory, with)) {
fileList.incFilesInBackupFolder();
return;
}
if (checkedReplace.containsKey(matched)) {
logger.info(intl("local-backup-replace-duplicate"), "file-path", matched);
return;
}
checkedReplace.put(matched, withPath.toString());
return;
}
logger.info(intl("local-backup-replace-skipped"), "file-path", file.getPath(), "with-path", with.getPath());
});
} else if (!replace.isEmpty()) {
logger.info(intl("local-backup-replace-not-folder"), "location", location);
}
int filesInBackupFolder = fileList.getFilesInBackupFolder();
if (filesInBackupFolder > 0) {
logger.info(
Expand All @@ -116,7 +153,7 @@ public void makeBackup(@NotNull String location, LocalDateTimeFormatter formatte
String lastFolderName = location.substring(lastSeparatorIndex + 1);
fileName = fileName.replace(NAME_KEYWORD, lastFolderName);
}
zipIt(location, path.getPath() + "/" + fileName, fileList);
zipIt(location, path.getPath() + "/" + fileName, fileList, checkedReplace);
}

/**
Expand Down Expand Up @@ -176,7 +213,7 @@ public void pruneLocalBackups(String location, LocalDateTimeFormatter formatter)
* @param outputFilePath the path of the folder to put it in
* @param fileList file to include in the zip
*/
private void zipIt(String inputFolderPath, String outputFilePath, BackupFileList fileList) throws Exception {
private void zipIt(String inputFolderPath, String outputFilePath, BackupFileList fileList, Map<String, String> replace) throws Exception {
byte[] buffer = new byte[1024];
FileOutputStream fileOutputStream;
ZipOutputStream zipOutputStream = null;
Expand All @@ -190,7 +227,7 @@ private void zipIt(String inputFolderPath, String outputFilePath, BackupFileList
zipOutputStream.setLevel(ConfigParser.getConfig().backupStorage.zipCompression);
for (String file : fileList.getList()) {
ZipEntry entry = new ZipEntry(formattedInputFolderPath + "/" + file);
String filePath = inputFolderPath + "/" + file;
String filePath = inputFolderPath + "/" + replace.getOrDefault(file, file);
BasicFileAttributes fileAttributes = null;
try {
fileAttributes = Files.readAttributes(Paths.get(filePath), BasicFileAttributes.class);
Expand Down Expand Up @@ -325,6 +362,31 @@ private static String escapeBackupLocation(@NotNull String location) {
return location.replace("../", "");
}

private static boolean isUnsafeRelativePath(@NotNull String path) {
if (path.isEmpty()) return true;
Path p = Paths.get(path);
if (p.isAbsolute()) return true;
for (Path seg : p) {
if (seg.toString().equals("..")) return true;
}
return false;
}

/**
* Whether the target file's real (symlink-resolved) path is still contained within location.
* Guards against a symlink inside location pointing outside of it, which a purely
* string-based check like {@link #isUnsafeRelativePath} cannot detect.
*/
private static boolean isWithinLocation(String location, File target) {
try {
Path root = new File(location).getCanonicalFile().toPath();
Path real = target.getCanonicalFile().toPath();
return real.startsWith(root);
} catch (IOException e) {
return false;
}
}

/**
* Finds all folders that match a glob
* @param glob the glob to search
Expand Down
6 changes: 6 additions & 0 deletions DriveBackup/src/main/resources/intl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ backup-failed-absolute-path: |-
backup-file-upload-complete: 'Upload(s) for file "<file-name>" complete'
backup-file-upload-start: 'Starting upload(s) for file "<file-name>"'
backup-forced: "Forcing a backup"
backup-list-replace-invalid: "Replace invalid in backup entry <entry>, leaving blank"
backup-list-replace-unsafe-path: 'Skipping unsafe replace path "<file-path>", it must be a relative path without ".."'
backup-list-blacklist-invalid: "Blacklist invalid in backup entry <entry>, leaving blank"
backup-list-format-invalid: "Format invalid, skipping backup list entry <entry>"
backup-list-glob-invalid: "Glob invalid, skipping backup list entry <entry>"
Expand Down Expand Up @@ -198,6 +200,10 @@ local-backup-limit-reached: "There are <backup-count> file(s) which exceeds the
local-backup-no-limit: "Local backup limit is set to 0, skipping pruning"
local-backup-pruning-complete: 'Local backup pruning complete for "<location>"'
local-backup-pruning-start: 'Pruning local backups for "<location>"'
local-backup-replace-duplicate: 'Multiple replace entries target "<file-path>" in the backup, using the first one'
local-backup-replace-escapes-location: 'Skipping replace of "<file-path>", it resolves outside of location "<location>" (possibly via a symlink)'
local-backup-replace-not-folder: 'Ignoring replace for "<location>", it must be a folder to use replace'
local-backup-replace-skipped: 'Skipping replace of "<file-path>" with "<with-path>", make sure both files exist and "<file-path>" is not blacklisted'
local-keep-count-invalid: "Inputted local keep count invalid, using default"
local-save-directory-not-relative: "Local save directory is not relative, making relative to server directory"
location-empty: "Location <location> is empty, skipping"
Expand Down
Loading