mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2026-10-25 14:11:01 +00:00
Merge pull request #280 from AppleCommander/feature/acx-backup
Basic automated backup feature for 'ac' and 'acx'. It can be manually configured for AppleCommander itself at this point, but with all the GUI changes it feels better to merge first and then attack the graphical component.
This commit is contained in:
11 files changed
+171
-26
No files matched your search
@@ -50,6 +50,7 @@ import org.applecommander.source.DataBufferSource;
|
||||
import org.applecommander.source.FileSource;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.source.Sources;
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
import org.applecommander.util.Information;
|
||||
|
||||
import java.io.*;
|
||||
@@ -99,8 +100,7 @@ import java.util.Queue;
|
||||
* </pre>
|
||||
*
|
||||
* @author John B. Matthews
|
||||
*
|
||||
* Changed at: Dec 1, 2017
|
||||
* @author Robert Greene
|
||||
* @author Lisias Toledo
|
||||
*/
|
||||
public class ac {
|
||||
@@ -189,6 +189,14 @@ public class ac {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(FormattedDisk formattedDisk) throws IOException {
|
||||
if (formattedDisk.hasChanged()) {
|
||||
BackupStrategy backupStrategy = BackupStrategy.create(System.getenv("AC_BACKUP_STRATEGY"));
|
||||
backupStrategy.backup(formattedDisk.getFilename());
|
||||
formattedDisk.save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the AppleSoft BASIC program from text into it's "native" tokenized format.
|
||||
@@ -226,7 +234,7 @@ public class ac {
|
||||
if (entry.needsAddress()) {
|
||||
entry.setAddress(config.startAddress);
|
||||
}
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +267,7 @@ public class ac {
|
||||
if (entry.needsAddress()) {
|
||||
entry.setAddress(stringToInt(address));
|
||||
}
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,7 +323,7 @@ public class ac {
|
||||
if (entry.needsAddress()) {
|
||||
entry.setAddress(stringToInt(address));
|
||||
}
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
} else {
|
||||
throw new IOException("Unable to create entry...");
|
||||
}
|
||||
@@ -407,7 +415,7 @@ public class ac {
|
||||
FileEntry entry = name.getEntry(formattedDisk);
|
||||
if (entry != null) {
|
||||
entry.delete();
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
} else {
|
||||
System.err.println(textBundle.format(
|
||||
"CommandLineNoMatchMessage", name.fullName)); //$NON-NLS-1$
|
||||
@@ -568,7 +576,7 @@ public class ac {
|
||||
FileEntry entry = name.getEntry(formattedDisk);
|
||||
if (entry != null) {
|
||||
entry.setLocked(lockState);
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
} else {
|
||||
System.err.println(textBundle.format(
|
||||
"CommandLineNoMatchMessage", name.fullName)); //$NON-NLS-1$
|
||||
@@ -590,7 +598,7 @@ public class ac {
|
||||
if (!source.isAny(Hint.DISK_COPY_IMAGE, Hint.ORIGIN_SHRINKIT, Hint.UNIVERSAL_DISK_IMAGE)) {
|
||||
FormattedDisk formattedDisk = ctx.disks.getFirst();
|
||||
formattedDisk.setDiskName(volName);
|
||||
formattedDisk.save();
|
||||
save(formattedDisk);
|
||||
}
|
||||
else
|
||||
throw new IOException(textBundle.get("CommandLineSDKReadOnly"));
|
||||
@@ -604,7 +612,7 @@ public class ac {
|
||||
Source source = DataBufferSource.create(imageSize, fileName).hints(Hint.DOS_SECTOR_ORDER).get();
|
||||
TrackSectorDevice device = new DosOrderedTrackSectorDevice(source, Hint.DOS_SECTOR_ORDER);
|
||||
FormattedDisk[] disks = DosFormatDisk.create(fileName, device);
|
||||
disks[0].save();
|
||||
save(disks[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,7 +623,7 @@ public class ac {
|
||||
Source source = DataBufferSource.create(imageSize, fileName).hints(Hint.PRODOS_BLOCK_ORDER).get();
|
||||
BlockDevice device = new ProdosOrderedBlockDevice(source, BlockDevice.STANDARD_BLOCK_SIZE);
|
||||
FormattedDisk[] disks = PascalFormatDisk.create(fileName, volName, device);
|
||||
disks[0].save();
|
||||
save(disks[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -626,7 +634,7 @@ public class ac {
|
||||
Source source = DataBufferSource.create(imageSize, fileName).hints(Hint.PRODOS_BLOCK_ORDER).get();
|
||||
BlockDevice device = new ProdosOrderedBlockDevice(source, BlockDevice.STANDARD_BLOCK_SIZE);
|
||||
FormattedDisk[] disks = ProdosFormatDisk.create(fileName, volName, device);
|
||||
disks[0].save();
|
||||
save(disks[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,7 +668,7 @@ public class ac {
|
||||
DiskFactory.Context ctx = Disks.inspect(source);
|
||||
FormattedDisk disk = ctx.disks.getFirst();
|
||||
disk.setFilename(imageName);
|
||||
disk.save();
|
||||
save(disk);
|
||||
}
|
||||
|
||||
static int stringToInt(String s) {
|
||||
|
||||
+11
-1
@@ -19,13 +19,23 @@
|
||||
*/
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
public abstract class ReadWriteDiskCommandOptions extends ReadOnlyDiskImageCommandOptions {
|
||||
@Option(names = "--backup", description = {
|
||||
"Automated backup when an image changes; use 'bak' to append '.bak' to file name;",
|
||||
"any other value is assumed to be a directory. [$ACX_BACKUP_STRATEGY]"
|
||||
}, defaultValue = "${ACX_BACKUP_STRATEGY}")
|
||||
private String backupCode = "";
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
int returnCode = handleCommand();
|
||||
|
||||
if (returnCode == 0) {
|
||||
saveDisk(selectedDisks().getFirst());
|
||||
BackupStrategy backupStrategy = BackupStrategy.create(backupCode);
|
||||
saveDisk(selectedDisks().getFirst(), backupStrategy);
|
||||
}
|
||||
|
||||
return returnCode;
|
||||
|
||||
+3
-1
@@ -20,6 +20,7 @@
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
@@ -43,11 +44,12 @@ public abstract class ReusableCommandOptions implements Callable<Integer> {
|
||||
|
||||
public abstract int handleCommand() throws Exception;
|
||||
|
||||
public void saveDisk(FormattedDisk disk) {
|
||||
public void saveDisk(FormattedDisk disk, BackupStrategy backupStrategy) {
|
||||
try {
|
||||
// Only save if there are changes.
|
||||
if (disk.getSource().hasChanged()) {
|
||||
LOG.fine(() -> String.format("Saving disk '%s'", disk.getFilename()));
|
||||
backupStrategy.backup(disk.getFilename());
|
||||
disk.save();
|
||||
} else {
|
||||
LOG.fine(() -> String.format("Disk '%s' has not changed; not saving.", disk.getFilename()));
|
||||
|
||||
@@ -22,6 +22,7 @@ package io.github.applecommander.acx.command;
|
||||
import com.webcodepro.applecommander.storage.DiskFactory;
|
||||
import com.webcodepro.applecommander.storage.Disks;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
import io.github.applecommander.acx.base.ReusableCommandOptions;
|
||||
import org.applecommander.source.Source;
|
||||
import org.applecommander.source.Sources;
|
||||
@@ -56,7 +57,7 @@ public class ConvertCommand extends ReusableCommandOptions {
|
||||
DiskFactory.Context ctx = Disks.inspect(source);
|
||||
FormattedDisk disk = ctx.disks.getFirst();
|
||||
disk.setFilename(diskName);
|
||||
saveDisk(disk);
|
||||
saveDisk(disk, BackupStrategy.none());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
|
||||
import io.github.applecommander.acx.OrderType;
|
||||
import io.github.applecommander.acx.SystemType;
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
import io.github.applecommander.acx.base.ReusableCommandOptions;
|
||||
import io.github.applecommander.acx.converter.DataSizeConverter;
|
||||
import org.applecommander.device.*;
|
||||
@@ -107,7 +108,7 @@ public class CreateDiskCommand extends ReusableCommandOptions {
|
||||
systemType.copySystem(disks[0], ctx.disks.getFirst());
|
||||
}
|
||||
|
||||
saveDisk(disks[0]);
|
||||
saveDisk(disks[0], BackupStrategy.none());
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* AppleCommander - An Apple ][ image utility.
|
||||
* Copyright (C) 2026 by Robert Greene and others
|
||||
* robgreene at users.sourceforge.net
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
package org.applecommander.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
|
||||
/// Provides a simple but automated backup strategy to the `acx` tool.
|
||||
///
|
||||
/// General usage pattern is as follows:
|
||||
/// ```java
|
||||
/// // Only save if there are changes.
|
||||
/// if (disk.getSource().hasChanged()) {
|
||||
/// backupStrategy.backup(disk.getFilename());
|
||||
/// disk.save();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To create a `BackupStrategy` in a consistent manner, use the #create
|
||||
/// method:
|
||||
/// ```'java
|
||||
/// BackupStrategy backupStrategy = BackupStrategy.create("bak");
|
||||
/// ```
|
||||
///
|
||||
/// If using in a context where a backup is no expected (as in a new file, or
|
||||
/// a "save as" situation), simply use `BackupStrategy.none()`.
|
||||
///
|
||||
/// The coding is as follows:
|
||||
/// * `null` or blank (`""`): Returns an ineffective strategy. (No branches in code.)
|
||||
/// * `bak`: Returns a backup strategy that renames the existing file from `name.dsk`
|
||||
/// to `name.dsk.bak`, overwriting any existing files.
|
||||
/// * The default strategy is to assume the value is a directory, _which will be created_,
|
||||
/// and make a copy of the file into this directory.
|
||||
///
|
||||
/// @see #create(String)
|
||||
/// @see #none()
|
||||
public interface BackupStrategy {
|
||||
void backup(String filename) throws IOException;
|
||||
|
||||
static BackupStrategy create(String backupCode) {
|
||||
if (backupCode == null) backupCode = "";
|
||||
return switch (backupCode) {
|
||||
case "" -> BackupStrategy.none();
|
||||
case "bak" -> BackupStrategy.fileExtension("bak");
|
||||
default -> BackupStrategy.directory(backupCode);
|
||||
};
|
||||
}
|
||||
|
||||
static BackupStrategy none() {
|
||||
return filename -> {
|
||||
// Do nothing
|
||||
};
|
||||
}
|
||||
|
||||
static BackupStrategy fileExtension(final String extension) {
|
||||
return filename -> {
|
||||
Path src = Path.of(filename);
|
||||
Path dst = Path.of(filename + "." + extension);
|
||||
Files.copy(src, dst, REPLACE_EXISTING);
|
||||
};
|
||||
}
|
||||
|
||||
static BackupStrategy directory(final String directory) {
|
||||
return filename -> {
|
||||
Path src = Path.of(filename);
|
||||
Path backupDir = Path.of(directory);
|
||||
Files.createDirectories(backupDir);
|
||||
Path dst = backupDir.resolve(src.getFileName());
|
||||
Files.copy(src, dst, REPLACE_EXISTING);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,10 @@ CommandLineHelp = \
|
||||
-pas140 <imagename> <volname> create a 140K Pascal image.\n\
|
||||
-pas800 <imagename> <volname> create an 800K Pascal image.\n\
|
||||
-convert <filename> <imagename> [<sizeblocks>] uncompress a ShrinkIt or Binary\n II file; or convert a DiskCopy 4.2 image into a ProDOS disk image.\n\
|
||||
-bas <imagename> <filename> import an AppleSoft basic file from text\n back to it's tokenized format.
|
||||
-bas <imagename> <filename> import an AppleSoft basic file from text\n back to it's tokenized format.\n\
|
||||
\n\
|
||||
Environment variables:\n\
|
||||
- AC_BACKUP_STRATEGY: 'bak' (mydisk.dsk -> mydisk.dsk.bak);\n anything else is assumed to be a directory.
|
||||
CommandLineSDKReadOnly = SDK, SHK, and DC42 files are read-only. Use the convert option on them first.
|
||||
|
||||
# UserPreferences
|
||||
|
||||
File renamed without changes.
+27
-6
@@ -20,6 +20,7 @@
|
||||
package com.webcodepro.applecommander.ui;
|
||||
|
||||
import com.webcodepro.applecommander.util.Host;
|
||||
import org.applecommander.util.BackupStrategy;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -33,11 +34,12 @@ import java.util.Properties;
|
||||
* @author Rob Greene
|
||||
*/
|
||||
public class UserPreferences {
|
||||
private static final String FILENAME = "AppleCommander.preferences"; //$NON-NLS-1$
|
||||
private static final String IMAGE_DIRECTORY = "imageDirectory"; //$NON-NLS-1$
|
||||
private static final String EXPORT_DIRECTORY = "exportDirectory"; //$NON-NLS-1$
|
||||
private static final String SAVE_DIRECTORY = "saveDirectory"; //$NON-NLS-1$
|
||||
private static final String IMPORT_DIRECTORY = "importDirectory"; //$NON-NLS-1$
|
||||
private static final String FILENAME = "AppleCommander.preferences";
|
||||
private static final String IMAGE_DIRECTORY = "imageDirectory";
|
||||
private static final String EXPORT_DIRECTORY = "exportDirectory";
|
||||
private static final String SAVE_DIRECTORY = "saveDirectory";
|
||||
private static final String IMPORT_DIRECTORY = "importDirectory";
|
||||
private static final String BACKUP_STRATEGY = "backupStrategy";
|
||||
private static UserPreferences instance;
|
||||
private final Properties properties = new Properties();
|
||||
/**
|
||||
@@ -77,7 +79,7 @@ public class UserPreferences {
|
||||
FileOutputStream outputStream =
|
||||
new FileOutputStream(Host.getPrefDir() + FILENAME);
|
||||
properties.store(outputStream, UiBundle.getInstance().
|
||||
get("UserPreferencesComment")); //$NON-NLS-1$
|
||||
get("UserPreferencesComment"));
|
||||
outputStream.close();
|
||||
} catch (Exception ignored) {
|
||||
// Ignored
|
||||
@@ -107,6 +109,19 @@ public class UserPreferences {
|
||||
public String getImportDirectory() {
|
||||
return properties.getProperty(IMPORT_DIRECTORY);
|
||||
}
|
||||
/**
|
||||
* Get the backup strategy text.
|
||||
*/
|
||||
public String getBackupStrategy() {
|
||||
return properties.getProperty(BACKUP_STRATEGY);
|
||||
}
|
||||
/**
|
||||
* Create the BackupStrategy object.
|
||||
*/
|
||||
public BackupStrategy createBackupStrategy() {
|
||||
return BackupStrategy.create(getBackupStrategy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the disk image directory.
|
||||
*/
|
||||
@@ -131,4 +146,10 @@ public class UserPreferences {
|
||||
public void setImportDirectory(String importDirectory) {
|
||||
properties.setProperty(IMPORT_DIRECTORY, importDirectory);
|
||||
}
|
||||
/**
|
||||
* Set the backup strategy code.
|
||||
*/
|
||||
public void setBackupStrategy(String backupStrategy) {
|
||||
properties.setProperty(BACKUP_STRATEGY, backupStrategy);
|
||||
}
|
||||
}
|
||||
+1
@@ -1393,6 +1393,7 @@ public class DiskExplorerTab {
|
||||
saveAs(); // no directory -> assume a new/unsaved image
|
||||
return;
|
||||
}
|
||||
userPreferences.createBackupStrategy().backup(disks[0].getFilename());
|
||||
disks[0].save();
|
||||
saveToolItem.setEnabled(disks[0].hasChanged());
|
||||
} catch (IOException ex) {
|
||||
|
||||
+9
-3
@@ -1,12 +1,18 @@
|
||||
# Usage: 'shell env.sh'
|
||||
|
||||
# Try to accommodate Mac and Linux
|
||||
platform=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
machine=$(uname -m | tr '[:upper:]' '[:lower:]')
|
||||
platform=$(uname | tr '[:upper:]' '[:lower:]' | sed 's/darwin/macosx/')
|
||||
machine=$(uname -m | tr '[:upper:]' '[:lower:]' | sed 's/arm64/aarch64/')
|
||||
|
||||
gui_flags=("--enable-native-access=ALL-UNNAMED")
|
||||
if [ "${platform}" = "macosx" ]
|
||||
then
|
||||
gui_flags+=("-XstartOnFirstThread")
|
||||
fi
|
||||
|
||||
# These are useful aliases while developing
|
||||
alias ac="java -jar ${PWD}/app/cli-ac/build/libs/AppleCommander-ac-*.jar"
|
||||
alias acn="${PWD}/app/cli-ac/build/native/nativeCompile/ac-*"
|
||||
alias acx="java -jar ${PWD}/app/cli-acx/build/libs/AppleCommander-acx-*.jar"
|
||||
alias acxn="${PWD}/app/cli-acx/build/native/nativeCompile/acx-*"
|
||||
alias acgui="java -jar ${PWD}/app/gui-swt-${platform}-${machine}/build/libs/AppleCommander-${platform}-${machine}-*.jar"
|
||||
alias acgui="java ${gui_flags[*]} -jar ${PWD}/app/gui-swt-${platform}-${machine}/build/libs/AppleCommander-${platform}-${machine}-*.jar"
|
||||
Reference in new issue
Block a user