diff --git a/app/cli-ac/src/main/java/com/webcodepro/applecommander/ui/ac.java b/app/cli-ac/src/main/java/com/webcodepro/applecommander/ui/ac.java index 08d608c3..0c128934 100644 --- a/app/cli-ac/src/main/java/com/webcodepro/applecommander/ui/ac.java +++ b/app/cli-ac/src/main/java/com/webcodepro/applecommander/ui/ac.java @@ -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; * * * @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) { diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReadWriteDiskCommandOptions.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReadWriteDiskCommandOptions.java index 81252563..ceb5f74a 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReadWriteDiskCommandOptions.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReadWriteDiskCommandOptions.java @@ -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; diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReusableCommandOptions.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReusableCommandOptions.java index 5fc25821..7e0ed8be 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReusableCommandOptions.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/base/ReusableCommandOptions.java @@ -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 { 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())); diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ConvertCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ConvertCommand.java index a4b05d2f..5b55f886 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ConvertCommand.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/ConvertCommand.java @@ -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; } diff --git a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CreateDiskCommand.java b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CreateDiskCommand.java index 353ced80..acccbf97 100644 --- a/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CreateDiskCommand.java +++ b/app/cli-acx/src/main/java/io/github/applecommander/acx/command/CreateDiskCommand.java @@ -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; } diff --git a/lib/ac-api/src/main/java/org/applecommander/util/BackupStrategy.java b/lib/ac-api/src/main/java/org/applecommander/util/BackupStrategy.java new file mode 100644 index 00000000..079994a5 --- /dev/null +++ b/lib/ac-api/src/main/java/org/applecommander/util/BackupStrategy.java @@ -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); + }; + } + } \ No newline at end of file diff --git a/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties b/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties index 182eea36..15fc64af 100644 --- a/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties +++ b/lib/ac-api/src/main/resources/com/webcodepro/applecommander/ui/UiBundle.properties @@ -128,7 +128,10 @@ CommandLineHelp = \ -pas140 create a 140K Pascal image.\n\ -pas800 create an 800K Pascal image.\n\ -convert [] uncompress a ShrinkIt or Binary\n II file; or convert a DiskCopy 4.2 image into a ProDOS disk image.\n\ - -bas import an AppleSoft basic file from text\n back to it's tokenized format. + -bas 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 diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/ui/ImportSpecification.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/ImportSpecification.java similarity index 100% rename from lib/ac-api/src/main/java/com/webcodepro/applecommander/ui/ImportSpecification.java rename to lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/ImportSpecification.java diff --git a/lib/ac-api/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java similarity index 84% rename from lib/ac-api/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java rename to lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java index 3373b25d..7406ed01 100644 --- a/lib/ac-api/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/UserPreferences.java @@ -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); + } } diff --git a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/DiskExplorerTab.java b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/DiskExplorerTab.java index d6258df3..7a024e66 100644 --- a/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/DiskExplorerTab.java +++ b/lib/ac-swt-common/src/main/java/com/webcodepro/applecommander/ui/swt/DiskExplorerTab.java @@ -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) { diff --git a/scripts/env.sh b/scripts/env.sh index 2b3696fc..3eae9583 100644 --- a/scripts/env.sh +++ b/scripts/env.sh @@ -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"