mirror of
https://github.com/AppleCommander/AppleCommander.git
synced 2024-12-21 01:30:34 +00:00
Merging in 'acx' tool, merging gradle builds
This commit is contained in:
parent
7469d90c8a
commit
acf3874185
150
app/ac-acx/README.md
Normal file
150
app/ac-acx/README.md
Normal file
@ -0,0 +1,150 @@
|
||||
# `ac`, extended
|
||||
|
||||
This is a revamp of the venerable `ac` command-line utility with a more modern command-line interface.
|
||||
|
||||
# Sample Run
|
||||
|
||||
All of the commands use `-d` to designate the disk. If there are many commands, setting the environment variable `ACX_DISK_NAME` can be used to simplify commands.
|
||||
|
||||
For example, this sequence:
|
||||
1. Creates a ProDOS disk image (pulling ProDOS from the 2.4.2 system master).
|
||||
2. Imports and tokenizes the sample `startup.bas` program and places it on the disk as `STARTUP`.
|
||||
3. Lists the resulting disk.
|
||||
|
||||
```
|
||||
$ cat > startup.bas
|
||||
10 TEXT:HOME:GR
|
||||
20 FOR Y=0 TO 3
|
||||
30 FOR X=0 TO 3
|
||||
40 COLOR=Y*4+X
|
||||
50 FOR A=0 TO 9
|
||||
60 HLIN X*10,X*10+9 AT Y*10+A
|
||||
70 NEXT A,X,Y
|
||||
80 END
|
||||
<ctrl+D>
|
||||
|
||||
$ export ACX_DISK_NAME=sample.po
|
||||
$ acx create --format=ProDOS_2_4_2.dsk --name=SAMPLES --size=140k --type=prodos
|
||||
$ acx import --basic startup.bas --name=STARTUP
|
||||
|
||||
$ acx list --native
|
||||
File: sample.po
|
||||
Name: /SAMPLES/
|
||||
* PRODOS SYS 035 01/02/2022 01/13/2018 17,128
|
||||
* BASIC.SYSTEM SYS 021 01/02/2022 01/13/2018 10,240 A=$2000
|
||||
STARTUP BAS 001 01/02/2022 01/02/2022 97 A=$0801
|
||||
ProDOS format; 110592 bytes free; 32768 bytes used.
|
||||
```
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
```
|
||||
$ acx --help
|
||||
Usage: acx [-hVv] [--debug] [--quiet] [COMMAND]
|
||||
|
||||
'ac' experimental utility
|
||||
|
||||
Options:
|
||||
--debug Show detailed stack traces.
|
||||
-h, --help Show this help message and exit.
|
||||
--quiet Turn off all logging.
|
||||
-v, --verbose Be verbose. Multiple occurrences increase logging.
|
||||
-V, --version Print version information and exit.
|
||||
|
||||
Commands:
|
||||
convert Uncompress a ShrinkIt or Binary II file;
|
||||
copy, cp Copy files between disks.
|
||||
create, mkdisk Rename volume of a disk image.
|
||||
delete, del, rm Delete file(s) from a disk image.
|
||||
diskmap, map Show disk usage map.
|
||||
export, x, get Export file(s) from a disk image.
|
||||
help Displays help information about the specified command
|
||||
import, put Import file onto disk.
|
||||
info, i Show information on a disk image(s).
|
||||
list, ls List directory of disk image(s).
|
||||
lock Lock file(s) on a disk image.
|
||||
mkdir, md Create a directory on disk.
|
||||
rename, ren Rename file on a disk image.
|
||||
rename-disk Rename volume of a disk image.
|
||||
rmdir, rd Remove a directory on disk.
|
||||
unlock Unlock file(s) on a disk image.
|
||||
```
|
||||
|
||||
## Info
|
||||
|
||||
```
|
||||
$ acx info --help
|
||||
Usage: acx info [-h] <paths>...
|
||||
|
||||
Show information on a disk image(s).
|
||||
|
||||
Parameters:
|
||||
<paths>... Image(s) to process.
|
||||
|
||||
Options:
|
||||
-h, --help Show help for subcommand.
|
||||
```
|
||||
|
||||
```
|
||||
$ acx info "Beagle Graphics.dsk"
|
||||
File Name: Beagle Graphics.dsk
|
||||
Disk Name: DISK VOLUME #254
|
||||
Physical Size (bytes): 143360
|
||||
Free Space (bytes): 20480
|
||||
Used Space (bytes): 122880
|
||||
Physical Size (KB): 140
|
||||
Free Space (KB): 20
|
||||
Used Space (KB): 120
|
||||
Archive Order: DOS
|
||||
Disk Format: DOS 3.3
|
||||
Total Sectors: 560
|
||||
Free Sectors: 80
|
||||
Used Sectors: 480
|
||||
Tracks On Disk: 35
|
||||
Sectors On Disk: 16
|
||||
```
|
||||
|
||||
## List
|
||||
|
||||
```
|
||||
$ acx list --help
|
||||
Usage: acx list [-hr] [--[no-]column] [--deleted] [--[no-]footer] [--[no-]
|
||||
header] [--globs=<globs>[,<globs>...]]... [-n | -s | -l]
|
||||
[--file | --directory] <paths>...
|
||||
|
||||
List directory of disk image(s).
|
||||
|
||||
Parameters:
|
||||
<paths>... Image(s) to process.
|
||||
|
||||
Options:
|
||||
--[no-]column Show column headers.
|
||||
--deleted Show deleted files.
|
||||
--directory Only include directories.
|
||||
--file Only include files.
|
||||
--[no-]footer Show footer.
|
||||
--globs=<globs>[,<globs>...]
|
||||
File glob(s) to match.
|
||||
-h, --help Show help for subcommand.
|
||||
--[no-]header Show header.
|
||||
-r, --[no-]recursive Display directory recursively.
|
||||
File display formatting:
|
||||
-l, --long, --detail Use long/detailed directory format.
|
||||
-n, --native Use native directory format (default).
|
||||
-s, --short, --standard Use brief directory format.
|
||||
```
|
||||
|
||||
```
|
||||
$ acx list --no-recursive DEVCD.HDV
|
||||
File: DEVCD.HDV
|
||||
Name: /DEV.CD/
|
||||
TOOLS DIR 002 06/25/1990 04/13/1989 1,024
|
||||
II.DISK.CENTRAL DIR 001 06/25/1990 04/13/1989 512
|
||||
UTILITIES DIR 002 06/25/1990 04/13/1989 1,024
|
||||
READ.ME.FIRST DIR 001 06/25/1990 04/21/1989 512
|
||||
GUIDED.TOURS DIR 001 06/25/1990 04/13/1989 512
|
||||
FINDER.DATA FND 001 06/25/1990 10/12/1989 172
|
||||
DEVELOP DIR 001 07/05/1990 06/25/1990 512
|
||||
ProDOS format; 1701376 bytes free; 19270144 bytes used.
|
||||
```
|
36
app/ac-acx/build.gradle
Normal file
36
app/ac-acx/build.gradle
Normal file
@ -0,0 +1,36 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'java'
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "info.picocli:picocli:$picocliVersion"
|
||||
implementation project(':lib:ac-api')
|
||||
implementation "net.sf.applecommander:ShrinkItArchive:$shkVersion"
|
||||
implementation "net.sf.applecommander:applesingle-api:$asVersion"
|
||||
implementation "net.sf.applecommander:bastools-api:$btVersion"
|
||||
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass = 'io.github.applecommander.acx.Main'
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveBaseName = 'AppleCommander'
|
||||
archiveAppendix = 'acx'
|
||||
manifest {
|
||||
attributes 'Implementation-Title': "AppleCommander 'acx'",
|
||||
'Implementation-Version': archiveVersion
|
||||
}
|
||||
from('../../LICENSE')
|
||||
}
|
BIN
app/ac-acx/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
app/ac-acx/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
app/ac-acx/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
app/ac-acx/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
101
app/ac-acx/src/main/java/io/github/applecommander/acx/Main.java
Normal file
101
app/ac-acx/src/main/java/io/github/applecommander/acx/Main.java
Normal file
@ -0,0 +1,101 @@
|
||||
package io.github.applecommander.acx;
|
||||
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogManager;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import io.github.applecommander.acx.command.ConvertCommand;
|
||||
import io.github.applecommander.acx.command.CopyFileCommand;
|
||||
import io.github.applecommander.acx.command.CreateDiskCommand;
|
||||
import io.github.applecommander.acx.command.DeleteCommand;
|
||||
import io.github.applecommander.acx.command.DiskMapCommand;
|
||||
import io.github.applecommander.acx.command.ExportCommand;
|
||||
import io.github.applecommander.acx.command.ImportCommand;
|
||||
import io.github.applecommander.acx.command.InfoCommand;
|
||||
import io.github.applecommander.acx.command.ListCommand;
|
||||
import io.github.applecommander.acx.command.LockCommand;
|
||||
import io.github.applecommander.acx.command.MkdirCommand;
|
||||
import io.github.applecommander.acx.command.RenameDiskCommand;
|
||||
import io.github.applecommander.acx.command.RenameFileCommand;
|
||||
import io.github.applecommander.acx.command.RmdirCommand;
|
||||
import io.github.applecommander.acx.command.UnlockCommand;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.HelpCommand;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
/**
|
||||
* Primary entry point into the 'acx' utility.
|
||||
*/
|
||||
@Command(name = "acx", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class,
|
||||
descriptionHeading = "%n",
|
||||
commandListHeading = "%nCommands:%n",
|
||||
optionListHeading = "%nOptions:%n",
|
||||
description = "'ac' experimental utility",
|
||||
subcommands = {
|
||||
ConvertCommand.class,
|
||||
CopyFileCommand.class,
|
||||
CreateDiskCommand.class,
|
||||
DeleteCommand.class,
|
||||
DiskMapCommand.class,
|
||||
ExportCommand.class,
|
||||
HelpCommand.class,
|
||||
ImportCommand.class,
|
||||
InfoCommand.class,
|
||||
ListCommand.class,
|
||||
LockCommand.class,
|
||||
MkdirCommand.class,
|
||||
RenameFileCommand.class,
|
||||
RenameDiskCommand.class,
|
||||
RmdirCommand.class,
|
||||
UnlockCommand.class
|
||||
})
|
||||
public class Main {
|
||||
private static Logger LOG = Logger.getLogger(Main.class.getName());
|
||||
private static final Level LOG_LEVELS[] = { Level.OFF, Level.SEVERE, Level.WARNING, Level.INFO,
|
||||
Level.CONFIG, Level.FINE, Level.FINER, Level.FINEST };
|
||||
|
||||
static {
|
||||
System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%n");
|
||||
setAllLogLevels(Level.WARNING);
|
||||
}
|
||||
private static void setAllLogLevels(Level level) {
|
||||
Logger rootLogger = LogManager.getLogManager().getLogger("");
|
||||
rootLogger.setLevel(level);
|
||||
for (Handler handler : rootLogger.getHandlers()) {
|
||||
handler.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
// This flag is read in PrintExceptionMessageHandler.
|
||||
@Option(names = { "--debug" }, description = "Show detailed stack traces.")
|
||||
static boolean enableStackTrace;
|
||||
|
||||
@Option(names = { "-v", "--verbose" }, description = "Be verbose. Multiple occurrences increase logging.")
|
||||
public void setVerbosity(boolean[] flag) {
|
||||
// The "+ 2" is due to the default of the levels
|
||||
int loglevel = Math.min(flag.length + 2, LOG_LEVELS.length);
|
||||
Level level = LOG_LEVELS[loglevel-1];
|
||||
setAllLogLevels(level);
|
||||
}
|
||||
|
||||
@Option(names = { "--quiet" }, description = "Turn off all logging.")
|
||||
public void setQuiet(boolean flag) {
|
||||
setAllLogLevels(Level.OFF);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CommandLine cmd = new CommandLine(new Main());
|
||||
cmd.setExecutionExceptionHandler(new PrintExceptionMessageHandler());
|
||||
if (args.length == 0) {
|
||||
cmd.usage(System.out);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
LOG.info(() -> String.format("Log level set to %s.", Logger.getGlobal().getLevel()));
|
||||
int exitCode = cmd.execute(args);
|
||||
LOG.fine("Exiting with code " + exitCode);
|
||||
System.exit(exitCode);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package io.github.applecommander.acx;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.IExecutionExceptionHandler;
|
||||
import picocli.CommandLine.ParseResult;
|
||||
|
||||
// Note: Taken from https://picocli.info/#_business_logic_exceptions
|
||||
public class PrintExceptionMessageHandler implements IExecutionExceptionHandler {
|
||||
public int handleExecutionException(Exception ex,
|
||||
CommandLine cmd,
|
||||
ParseResult parseResult) {
|
||||
|
||||
if (Main.enableStackTrace) {
|
||||
ex.printStackTrace(System.err);
|
||||
}
|
||||
else {
|
||||
// bold red error message
|
||||
cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage()));
|
||||
}
|
||||
|
||||
return cmd.getExitCodeExceptionMapper() != null
|
||||
? cmd.getExitCodeExceptionMapper().getExitCode(ex)
|
||||
: cmd.getCommandSpec().exitCodeOnExecutionException();
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package io.github.applecommander.acx;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.physical.ByteArrayImageLayout;
|
||||
import com.webcodepro.applecommander.storage.physical.DosOrder;
|
||||
import com.webcodepro.applecommander.storage.physical.ImageOrder;
|
||||
import com.webcodepro.applecommander.storage.physical.ProdosOrder;
|
||||
|
||||
import io.github.applecommander.acx.converter.DataSizeConverter;
|
||||
import io.github.applecommander.acx.fileutil.FileUtils;
|
||||
|
||||
public enum SystemType {
|
||||
DOS(SystemType::createDosImageOrder, SystemType::copyDosSystemTracks),
|
||||
OZDOS(SystemType::create800kDosImageOrder, SystemType::copyDosSystemTracks),
|
||||
UNIDOS(SystemType::create800kDosImageOrder, SystemType::copyDosSystemTracks),
|
||||
PRODOS(SystemType::createProdosImageOrder, SystemType::copyProdosSystemFiles),
|
||||
PASCAL(SystemType::createProdosImageOrder, SystemType::copyPascalSystemFiles);
|
||||
|
||||
private static Logger LOG = Logger.getLogger(SystemType.class.getName());
|
||||
|
||||
private Function<Integer,ImageOrder> createImageOrderFn;
|
||||
private BiConsumer<FormattedDisk,FormattedDisk> copySystemFn;
|
||||
|
||||
private SystemType(Function<Integer,ImageOrder> createImageOrderFn,
|
||||
BiConsumer<FormattedDisk,FormattedDisk> copySystemFn) {
|
||||
this.createImageOrderFn = createImageOrderFn;
|
||||
this.copySystemFn = copySystemFn;
|
||||
}
|
||||
|
||||
public ImageOrder createImageOrder(int size) {
|
||||
return createImageOrderFn.apply(size);
|
||||
}
|
||||
public void copySystem(FormattedDisk target, FormattedDisk source) {
|
||||
copySystemFn.accept(target, source);
|
||||
}
|
||||
|
||||
private static ImageOrder createDosImageOrder(int size) {
|
||||
ByteArrayImageLayout layout = new ByteArrayImageLayout(new byte[size]);
|
||||
return new DosOrder(layout);
|
||||
}
|
||||
private static ImageOrder create800kDosImageOrder(int size) {
|
||||
if (size != 800 * DataSizeConverter.KB) {
|
||||
LOG.warning("Setting image size to 800KB.");
|
||||
}
|
||||
ByteArrayImageLayout layout = new ByteArrayImageLayout(new byte[800 * DataSizeConverter.KB]);
|
||||
return new DosOrder(layout);
|
||||
}
|
||||
private static ImageOrder createProdosImageOrder(int size) {
|
||||
ByteArrayImageLayout layout = new ByteArrayImageLayout(size);
|
||||
return new ProdosOrder(layout);
|
||||
}
|
||||
|
||||
private static void copyDosSystemTracks(FormattedDisk targetDisk, FormattedDisk source) {
|
||||
DosFormatDisk target = (DosFormatDisk)targetDisk;
|
||||
// FIXME messing with the VTOC should be handled elsewhere
|
||||
byte[] vtoc = source.readSector(DosFormatDisk.CATALOG_TRACK, DosFormatDisk.VTOC_SECTOR);
|
||||
int sectorsPerTrack = vtoc[0x35];
|
||||
// Note that this also patches T0 S0 for BOOT0
|
||||
for (int t=0; t<3; t++) {
|
||||
for (int s=0; s<sectorsPerTrack; s++) {
|
||||
target.writeSector(t, s, source.readSector(t, s));
|
||||
target.setSectorUsed(t, s, vtoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void copyProdosSystemFiles(FormattedDisk target, FormattedDisk source) {
|
||||
// We need to explicitly fix the boot block
|
||||
target.writeBlock(0, source.readBlock(0));
|
||||
target.writeBlock(1, source.readBlock(1));
|
||||
|
||||
try {
|
||||
FileUtils copier = new FileUtils(false);
|
||||
for (String filename : Arrays.asList("PRODOS", "BASIC.SYSTEM")) {
|
||||
FileEntry sourceFile = source.getFile(filename);
|
||||
copier.copy(target, sourceFile);
|
||||
}
|
||||
} catch (DiskException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static void copyPascalSystemFiles(FormattedDisk target, FormattedDisk source) {
|
||||
// We need to explicitly fix the boot block
|
||||
target.writeBlock(0, source.readBlock(0));
|
||||
target.writeBlock(1, source.readBlock(1));
|
||||
|
||||
// TODO; uncertain what files Pascal disks require for booting
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.github.applecommander.acx;
|
||||
|
||||
import com.webcodepro.applecommander.ui.AppleCommander;
|
||||
|
||||
import io.github.applecommander.applesingle.AppleSingle;
|
||||
import picocli.CommandLine.IVersionProvider;
|
||||
|
||||
/** Display version information. Note that this is dependent on the Spring Boot Gradle plugin configuration. */
|
||||
public class VersionProvider implements IVersionProvider {
|
||||
public String[] getVersion() {
|
||||
return new String[] {
|
||||
String.format("acx: %s", Main.class.getPackage().getImplementationVersion()),
|
||||
String.format("AppleCommander API: %s", AppleCommander.VERSION),
|
||||
String.format("AppleSingle API: %s", AppleSingle.VERSION)
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
|
||||
import io.github.applecommander.acx.converter.DiskConverter;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
public abstract class ReadOnlyDiskImageCommandOptions extends ReusableCommandOptions {
|
||||
@Option(names = { "-d", "--disk" }, description = "Image to process [$ACX_DISK_NAME].", required = true,
|
||||
converter = DiskConverter.class, defaultValue = "${ACX_DISK_NAME}")
|
||||
protected Disk disk;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
public abstract class ReadWriteDiskCommandOptions extends ReadOnlyDiskImageCommandOptions {
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
int returnCode = handleCommand();
|
||||
|
||||
if (returnCode == 0) {
|
||||
saveDisk(disk);
|
||||
}
|
||||
|
||||
return returnCode;
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.applecommander.filestreamer.FileStreamer;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import io.github.applecommander.filestreamer.TypeOfFile;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
public abstract class ReadWriteDiskCommandWithGlobOptions extends ReadWriteDiskCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(ReadWriteDiskCommandWithGlobOptions.class.getName());
|
||||
|
||||
@Parameters(arity = "1..*", description = "File glob(s) to unlock (default = '*') - be cautious of quoting!")
|
||||
private List<String> globs = Arrays.asList("*");
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
List<FileTuple> files = FileStreamer.forDisk(disk)
|
||||
.ignoreErrors(true)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.matchGlobs(globs)
|
||||
.stream()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG.warning(() -> String.format("No matches found for %s.", String.join(",", globs)));
|
||||
} else {
|
||||
files.forEach(this::fileHandler);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public abstract void fileHandler(FileTuple tuple);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package io.github.applecommander.acx.base;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
@Command(descriptionHeading = "%n",
|
||||
optionListHeading = "%nOptions:%n",
|
||||
parameterListHeading = "%nParameters:%n")
|
||||
public abstract class ReusableCommandOptions implements Callable<Integer> {
|
||||
private static Logger LOG = Logger.getLogger(ReusableCommandOptions.class.getName());
|
||||
|
||||
@Option(names = { "-h", "--help" }, description = "Show help for subcommand.", usageHelp = true)
|
||||
private boolean helpFlag;
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
return handleCommand();
|
||||
}
|
||||
|
||||
public abstract int handleCommand() throws Exception;
|
||||
|
||||
public void saveDisk(Disk disk) {
|
||||
try {
|
||||
// Only save if there are changes.
|
||||
if (disk.getDiskImageManager().hasChanged()) {
|
||||
LOG.fine(() -> String.format("Saving disk '%s'", disk.getFilename()));
|
||||
disk.save();
|
||||
} else {
|
||||
LOG.fine(() -> String.format("Disk '%s' has not changed; not saving.", disk.getFilename()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.severe(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
|
||||
import io.github.applecommander.acx.base.ReusableCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "convert", description = {
|
||||
"Uncompress a ShrinkIt or Binary II file; ",
|
||||
"or convert a DiskCopy 4.2 image into a ProDOS disk image." })
|
||||
public class ConvertCommand extends ReusableCommandOptions {
|
||||
@Option(names = { "-d", "--disk" }, description = "Image to create [$ACX_DISK_NAME].", required = true,
|
||||
defaultValue = "${ACX_DISK_NAME}")
|
||||
private String diskName;
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Allow existing disk image to be replaced.")
|
||||
private boolean overwriteFlag;
|
||||
|
||||
@Parameters(description = "Archive to convert.", arity = "1")
|
||||
private String archiveName;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
File targetFile = new File(diskName);
|
||||
if (targetFile.exists() && !overwriteFlag) {
|
||||
throw new RuntimeException("File exists and overwriting not enabled.");
|
||||
}
|
||||
|
||||
Disk disk = new Disk(archiveName);
|
||||
disk.setFilename(diskName);
|
||||
saveDisk(disk);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import io.github.applecommander.acx.converter.DiskConverter;
|
||||
import io.github.applecommander.acx.fileutil.FileUtils;
|
||||
import io.github.applecommander.filestreamer.FileStreamer;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import io.github.applecommander.filestreamer.TypeOfFile;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "copy", description = "Copy files between disks.",
|
||||
aliases = { "cp" })
|
||||
public class CopyFileCommand extends ReadWriteDiskCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(CopyFileCommand.class.getName());
|
||||
|
||||
@Option(names = { "-r", "--recursive" }, description = "Copy files recursively.")
|
||||
private boolean recursiveFlag;
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Overwrite existing files.")
|
||||
private boolean overwriteFlag;
|
||||
|
||||
@Option(names = { "--to", "--directory" }, description = "Specify which directory to place files.")
|
||||
private String targetPath;
|
||||
|
||||
@Option(names = { "-s", "--from", "--source" }, description = "Source disk for files.",
|
||||
converter = DiskConverter.class, required = true)
|
||||
private Disk sourceDisk;
|
||||
|
||||
@Parameters(arity = "*", description = "File glob(s) to copy (default = '*')",
|
||||
defaultValue = "*")
|
||||
private List<String> globs;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
List<FileTuple> files = FileStreamer.forDisk(sourceDisk)
|
||||
.ignoreErrors(true)
|
||||
.includeTypeOfFile(TypeOfFile.BOTH)
|
||||
.recursive(recursiveFlag)
|
||||
.matchGlobs(globs)
|
||||
.stream()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG.warning(() -> String.format("No matches found for %s.", String.join(",", globs)));
|
||||
} else {
|
||||
files.forEach(this::fileHandler);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void fileHandler(FileTuple tuple) {
|
||||
try {
|
||||
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
|
||||
if (!recursiveFlag && tuple.fileEntry.isDirectory()) {
|
||||
formattedDisk.createDirectory(tuple.fileEntry.getFilename());
|
||||
} else {
|
||||
FileUtils copier = new FileUtils(overwriteFlag);
|
||||
copier.copy(formattedDisk, tuple.fileEntry);
|
||||
}
|
||||
} catch (DiskException ex) {
|
||||
LOG.severe(ex.getMessage());
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.OzDosFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.UniDosFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.physical.ImageOrder;
|
||||
|
||||
import io.github.applecommander.acx.SystemType;
|
||||
import io.github.applecommander.acx.base.ReusableCommandOptions;
|
||||
import io.github.applecommander.acx.converter.DataSizeConverter;
|
||||
import io.github.applecommander.acx.converter.SystemTypeConverter;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
@Command(name = "create", description = "Create a disk image.",
|
||||
aliases = { "mkdisk" })
|
||||
public class CreateDiskCommand extends ReusableCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(CreateDiskCommand.class.getName());
|
||||
|
||||
@Option(names = { "-d", "--disk" }, description = "Image to create [$ACX_DISK_NAME].", required = true,
|
||||
defaultValue = "${ACX_DISK_NAME}")
|
||||
private String imageName;
|
||||
|
||||
@Option(names = { "-t", "--type" }, required = true, converter = SystemTypeConverter.class,
|
||||
description = "Select system type (DOS, ProDOS, Pascal.")
|
||||
private SystemType type;
|
||||
|
||||
@Option(names = { "-s", "--size" }, defaultValue = "140kb", converter = DataSizeConverter.class,
|
||||
description = "Select disk size (140K, 800K, 10M).")
|
||||
private int size;
|
||||
|
||||
@Option(names = { "-f", "--format" },
|
||||
description = "Disk to copy system files/tracks/boot sector from.")
|
||||
private String formatSource;
|
||||
|
||||
@Option(names = { "-n", "--name" }, defaultValue = "NEW.DISK",
|
||||
description = "Disk Volume name (ProDOS/Pascal).")
|
||||
private String diskName;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
LOG.info(() -> String.format("Creating %s image of type %s.", DataSizeConverter.format(size), type));
|
||||
|
||||
ImageOrder order = type.createImageOrder(size);
|
||||
FormattedDisk[] disks = null;
|
||||
switch (type) {
|
||||
case DOS:
|
||||
disks = DosFormatDisk.create(imageName, order);
|
||||
break;
|
||||
case OZDOS:
|
||||
disks = OzDosFormatDisk.create(imageName, order);
|
||||
break;
|
||||
case UNIDOS:
|
||||
disks = UniDosFormatDisk.create(imageName, order);
|
||||
break;
|
||||
case PRODOS:
|
||||
disks = ProdosFormatDisk.create(imageName, diskName, order);
|
||||
break;
|
||||
case PASCAL:
|
||||
disks = PascalFormatDisk.create(imageName, diskName, order);
|
||||
break;
|
||||
}
|
||||
|
||||
if (formatSource != null) {
|
||||
Disk systemSource = new Disk(formatSource);
|
||||
type.copySystem(disks[0], systemSource.getFormattedDisks()[0]);
|
||||
}
|
||||
|
||||
saveDisk(disks[0]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
@Command(name = "delete", description = "Delete file(s) from a disk image.",
|
||||
aliases = { "del", "rm" })
|
||||
public class DeleteCommand extends ReadWriteDiskCommandWithGlobOptions {
|
||||
private static Logger LOG = Logger.getLogger(DeleteCommand.class.getName());
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Force delete locked files.")
|
||||
private boolean forceFlag;
|
||||
|
||||
public void fileHandler(FileTuple tuple) {
|
||||
if (tuple.fileEntry.isLocked()) {
|
||||
if (forceFlag) {
|
||||
LOG.info(() -> String.format("File '%s' is locked, but 'force' specified; ignoring lock.",
|
||||
tuple.fileEntry.getFilename()));
|
||||
} else {
|
||||
LOG.warning(() -> String.format("File '%s' is locked.", tuple.fileEntry.getFilename()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
tuple.fileEntry.delete();
|
||||
LOG.info(() -> String.format("File '%s' deleted.", tuple.fileEntry.getFilename()));
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk.DiskUsage;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
|
||||
@Command(name = "diskmap", description = "Show disk usage map.",
|
||||
aliases = { "map" })
|
||||
public class DiskMapCommand extends ReadOnlyDiskImageCommandOptions {
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
Arrays.asList(disk.getFormattedDisks()).forEach(this::showDiskMap);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void showDiskMap(FormattedDisk formattedDisk) {
|
||||
final int[] dimensions = formattedDisk.getBitmapDimensions();
|
||||
final int length = formattedDisk.getBitmapLength();
|
||||
final int width,height;
|
||||
final Function<Integer,Integer> leftNumFn, rightNumFn;
|
||||
if (dimensions != null && dimensions.length == 2) {
|
||||
height = dimensions[0];
|
||||
width = dimensions[1];
|
||||
// This is expected to be Track, so same number on left and right.
|
||||
leftNumFn = rightNumFn = i -> i;
|
||||
} else {
|
||||
width = 70;
|
||||
height= (length + width - 1) / width;
|
||||
// This is expected to be blocks, so show start of range of
|
||||
// left and end of range on right.
|
||||
leftNumFn = i -> i * width;
|
||||
rightNumFn = i -> (i + 1) * width - 1;
|
||||
}
|
||||
|
||||
title(formattedDisk.getBitmapLabels());
|
||||
header1(width); // 10's position
|
||||
header2(width); // 1's position
|
||||
header3(width); // divider
|
||||
|
||||
DiskUsage diskUsage = formattedDisk.getDiskUsage();
|
||||
for (int y=0; y<height; y++) {
|
||||
System.out.printf("%5d|", leftNumFn.apply(y));
|
||||
for (int x=0; x<width; x++) {
|
||||
if (diskUsage.hasNext()) {
|
||||
diskUsage.next();
|
||||
System.out.print(diskUsage.isUsed() ? '*' : '.');
|
||||
} else {
|
||||
System.out.print(" ");
|
||||
}
|
||||
}
|
||||
System.out.printf("|%d", rightNumFn.apply(y));
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
header3(width);
|
||||
header2(width);
|
||||
header1(width);
|
||||
}
|
||||
|
||||
void title(String[] labels) {
|
||||
System.out.print(" ");
|
||||
if (labels.length == 2) {
|
||||
System.out.printf("X=%s, Y=%s", labels[1], labels[0]);
|
||||
}
|
||||
else {
|
||||
System.out.printf("By %s", String.join(", ", labels));
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
void header1(final int width) {
|
||||
System.out.print(" ");
|
||||
for (int i=0; i<width; i++) {
|
||||
System.out.print(i % 10 == 0 ? Character.forDigit(i%10, 10) : ' ');
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
void header2(final int width) {
|
||||
System.out.print(" ");
|
||||
for (int i=0; i<width; i++) {
|
||||
System.out.print(i%10);
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
void header3(final int width) {
|
||||
System.out.print(" ");
|
||||
for (int i=0; i<width; i++) {
|
||||
System.out.print(i%5 == 0 ? '+' : '-');
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FileFilter;
|
||||
import com.webcodepro.applecommander.storage.filters.BinaryFileFilter;
|
||||
import com.webcodepro.applecommander.storage.filters.HexDumpFileFilter;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
|
||||
import io.github.applecommander.filestreamer.FileStreamer;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import io.github.applecommander.filestreamer.TypeOfFile;
|
||||
import io.github.applecommander.filters.AppleSingleFileFilter;
|
||||
import io.github.applecommander.filters.RawFileFilter;
|
||||
import picocli.CommandLine.ArgGroup;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.Parameters;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
@Command(name = "export", description = "Export file(s) from a disk image.",
|
||||
aliases = { "x", "get" })
|
||||
public class ExportCommand extends ReadOnlyDiskImageCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(ExportCommand.class.getName());
|
||||
|
||||
@Spec
|
||||
private CommandSpec spec;
|
||||
|
||||
@ArgGroup(exclusive = true, heading = "%nFile extract methods:%n")
|
||||
private FileExtractMethods extraction = new FileExtractMethods();
|
||||
|
||||
@Option(names = { "--deleted" }, description = "Include deleted files (use at your own risk!)")
|
||||
private boolean deletedFlag;
|
||||
|
||||
@Option(names = { "-o", "--output" }, description = "Extract to file or to directory (default is stdout).")
|
||||
private File outputFile;
|
||||
|
||||
@Parameters(arity = "*", description = "File glob(s) to extract (default = '*') - be cautious of quoting!")
|
||||
private List<String> globs = Arrays.asList("*");
|
||||
|
||||
public void validate() {
|
||||
List<String> errors = new ArrayList<>();
|
||||
// multiple files require --output
|
||||
if (isMultipleFiles()) {
|
||||
if (outputFile == null) {
|
||||
errors.add("--output directory must be specified with multiple files");
|
||||
} else if (!outputFile.isDirectory()) {
|
||||
errors.add("--output must be a directory");
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ParameterException(spec.commandLine(), String.join(", ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
validate();
|
||||
|
||||
Consumer<FileTuple> fileHandler =
|
||||
(outputFile == null) ? this::writeToStdout : this::writeToOutput;
|
||||
|
||||
FileStreamer.forDisk(disk)
|
||||
.ignoreErrors(true)
|
||||
.includeDeleted(deletedFlag)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.matchGlobs(globs)
|
||||
.stream()
|
||||
.forEach(fileHandler);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean hasFiles() {
|
||||
return globs != null && globs.size() > 1;
|
||||
}
|
||||
public boolean isAllFiles() {
|
||||
return globs == null || globs.isEmpty();
|
||||
}
|
||||
public boolean isMultipleFiles() {
|
||||
return hasFiles() || isAllFiles();
|
||||
}
|
||||
|
||||
public void writeToStdout(FileTuple tuple) {
|
||||
try {
|
||||
FileFilter ff = extraction.extractFunction.apply(tuple.fileEntry);
|
||||
System.out.write(ff.filter(tuple.fileEntry));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
public void writeToOutput(FileTuple tuple) {
|
||||
File file = outputFile;
|
||||
FileFilter ff = extraction.extractFunction.apply(tuple.fileEntry);
|
||||
if (file.isDirectory()) {
|
||||
if (!tuple.paths.isEmpty()) {
|
||||
file = new File(outputFile, String.join(File.pathSeparator, tuple.paths));
|
||||
boolean created = file.mkdirs();
|
||||
if (created) LOG.info(String.format("Directory created: %s", file.getPath()));
|
||||
}
|
||||
file = new File(file, ff.getSuggestedFileName(tuple.fileEntry));
|
||||
}
|
||||
LOG.info(String.format("Writing to '%s'", file.getPath()));
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
out.write(ff.filter(tuple.fileEntry));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileExtractMethods {
|
||||
private Function<FileEntry,FileFilter> extractFunction = this::asSuggestedFile;
|
||||
|
||||
@Option(names = { "--raw", "--binary" }, description = "Extract file in native format.")
|
||||
public void setBinaryExtraction(boolean flag) {
|
||||
this.extractFunction = this::asRawFile;
|
||||
}
|
||||
@Option(names = { "--hex", "--dump" }, description = "Extract file in hex dump format.")
|
||||
public void setHexDumpExtraction(boolean flag) {
|
||||
this.extractFunction = this::asHexDumpFile;
|
||||
}
|
||||
@Option(names = { "--suggested" }, description = "Extract file as suggested by AppleCommander (default)")
|
||||
public void setSuggestedExtraction(boolean flag) {
|
||||
this.extractFunction = this::asSuggestedFile;
|
||||
}
|
||||
@Option(names = { "--as", "--applesingle" }, description = "Extract file to AppleSingle file.")
|
||||
public void setAppleSingleExtraction(boolean flag) {
|
||||
this.extractFunction = this::asAppleSingleFile;
|
||||
}
|
||||
|
||||
public FileFilter asRawFile(FileEntry entry) {
|
||||
return new RawFileFilter();
|
||||
}
|
||||
public FileFilter asSuggestedFile(FileEntry entry) {
|
||||
FileFilter ff = entry.getSuggestedFilter();
|
||||
if (ff instanceof BinaryFileFilter) {
|
||||
ff = new HexDumpFileFilter();
|
||||
}
|
||||
return ff;
|
||||
}
|
||||
public FileFilter asHexDumpFile(FileEntry entry) {
|
||||
return new HexDumpFileFilter();
|
||||
}
|
||||
public FileFilter asAppleSingleFile(FileEntry entry) {
|
||||
return new AppleSingleFileFilter();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,330 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
|
||||
import com.webcodepro.applecommander.util.AppleUtil;
|
||||
import com.webcodepro.applecommander.util.StreamUtil;
|
||||
import com.webcodepro.applecommander.util.TranslatorStream;
|
||||
import com.webcodepro.shrinkit.HeaderBlock;
|
||||
import com.webcodepro.shrinkit.NuFileArchive;
|
||||
import com.webcodepro.shrinkit.ThreadRecord;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import io.github.applecommander.acx.converter.IntegerTypeConverter;
|
||||
import io.github.applecommander.acx.fileutil.FileEntryReader;
|
||||
import io.github.applecommander.acx.fileutil.FileUtils;
|
||||
import io.github.applecommander.acx.fileutil.OverrideFileEntryReader;
|
||||
import io.github.applecommander.applesingle.AppleSingle;
|
||||
import io.github.applecommander.applesingle.FileDatesInfo;
|
||||
import io.github.applecommander.applesingle.ProdosFileInfo;
|
||||
import io.github.applecommander.bastools.api.Configuration;
|
||||
import io.github.applecommander.bastools.api.Parser;
|
||||
import io.github.applecommander.bastools.api.TokenReader;
|
||||
import io.github.applecommander.bastools.api.Visitors;
|
||||
import io.github.applecommander.bastools.api.model.Program;
|
||||
import io.github.applecommander.bastools.api.model.Token;
|
||||
import picocli.CommandLine.ArgGroup;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "import", description = "Import file onto disk.",
|
||||
aliases = { "put" })
|
||||
public class ImportCommand extends ReadWriteDiskCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(ImportCommand.class.getName());
|
||||
|
||||
@ArgGroup(heading = "%nInput source:%n", multiplicity = "1")
|
||||
private InputData inputData;
|
||||
|
||||
@ArgGroup(heading = "%nProcessing options:%n")
|
||||
private Processor processor;
|
||||
|
||||
@ArgGroup(heading = "%nGeneral overrides:%n", exclusive = false)
|
||||
private Overrides overrides = new Overrides();
|
||||
|
||||
@Option(names = { "--dir" }, description = "Write file(s) to directory.")
|
||||
private Optional<String> directoryName;
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Over-write existing files.")
|
||||
private boolean overwriteFlag;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
DirectoryEntry directory = disk.getFormattedDisks()[0];
|
||||
if (directoryName.isPresent()) {
|
||||
String[] dirs = directoryName.get().split("/");
|
||||
for (String dir : dirs) {
|
||||
Optional<FileEntry> fileEntry = directory.getFiles().stream()
|
||||
.filter(f -> dir.equalsIgnoreCase(f.getFilename()))
|
||||
.findFirst();
|
||||
Optional<DirectoryEntry> dirEntry = fileEntry
|
||||
.filter(FileEntry::isDirectory)
|
||||
.map(DirectoryEntry.class::cast);
|
||||
directory = dirEntry.orElseThrow(() ->
|
||||
new IOException(String.format("Directory '%s' not found.", dir)));
|
||||
}
|
||||
}
|
||||
|
||||
FileUtils copier = new FileUtils(overwriteFlag);
|
||||
FileEntryReader inputReader = inputData.get();
|
||||
for (FileEntryReader processorReader : processor.apply(inputReader)) {
|
||||
FileEntryReader reader = OverrideFileEntryReader.builder()
|
||||
.filename(overrides.fileName)
|
||||
.prodosFiletype(overrides.fileType)
|
||||
.binaryAddress(overrides.fileAddress)
|
||||
.build(processorReader);
|
||||
|
||||
copier.copyFile(directory, reader);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static class InputData {
|
||||
private FileEntryReader fileEntryReader;
|
||||
|
||||
public FileEntryReader get() {
|
||||
return fileEntryReader;
|
||||
}
|
||||
|
||||
@Option(names = { "--stdin" }, description = "Import from standard input.")
|
||||
public void stdinFlag(boolean flag) {
|
||||
try {
|
||||
byte[] data = System.in.readAllBytes();
|
||||
fileEntryReader = OverrideFileEntryReader.builder()
|
||||
.fileData(data)
|
||||
.filename("UNKNOWN")
|
||||
.prodosFiletype("BIN")
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Parameters(description = "File to import.")
|
||||
public void fromFile(final String filename) {
|
||||
try {
|
||||
Path path = Path.of(filename);
|
||||
byte[] data = Files.readAllBytes(path);
|
||||
fileEntryReader = OverrideFileEntryReader.builder()
|
||||
.fileData(data)
|
||||
.filename(filename)
|
||||
.prodosFiletype("BIN")
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Processor {
|
||||
private Function<FileEntryReader,List<FileEntryReader>> fileEntryReaderFn;
|
||||
|
||||
public List<FileEntryReader> apply(FileEntryReader reader) {
|
||||
return fileEntryReaderFn.apply(reader);
|
||||
}
|
||||
|
||||
@Option(names = { "--text", "--text-high" }, description = {
|
||||
"Import as a text file, setting high bit and ",
|
||||
"replacing newline characters with $8D." })
|
||||
public void setTextModeSetHighBit(boolean textFlag) {
|
||||
fileEntryReaderFn = this::handleTextModeSetHighBit;
|
||||
}
|
||||
|
||||
@Option(names = { "--text-low" }, description = {
|
||||
"Import as a text file, clearing high bit and ",
|
||||
"replacing newline characters with $8D." })
|
||||
public void setTextModeClearHighBit(boolean textFlag) {
|
||||
fileEntryReaderFn = this::handleTextModeClearHighBit;
|
||||
}
|
||||
|
||||
@Option(names = { "--dos" }, description = "Use standard 4-byte DOS header.")
|
||||
public void setDosMode(boolean dosFlag) {
|
||||
fileEntryReaderFn = this::handleDosMode;
|
||||
}
|
||||
|
||||
@Option(names = { "--geos" }, description = "Interpret as a GEOS conversion file.")
|
||||
public void setGeosMode(boolean geosFlag) {
|
||||
fileEntryReaderFn = this::handleGeosMode;
|
||||
}
|
||||
|
||||
@Option(names = { "--basic" }, description = "Tokenize an AppleSoft BASIC program.")
|
||||
public void setApplesoftTokenizerMode(boolean tokenizeMode) {
|
||||
fileEntryReaderFn = this::handleApplesoftTokenizeMode;
|
||||
}
|
||||
|
||||
@Option(names = { "--as", "--applesingle" }, description = "Import Apple Single file.")
|
||||
public void setAppleSingleMode(boolean applesingleMode) {
|
||||
fileEntryReaderFn = this::handleAppleSingleMode;
|
||||
}
|
||||
|
||||
@Option(names = { "--shk", "--nufx", "--shrinkit", "--bxy" },
|
||||
description = "Import files from SHK archive.")
|
||||
public void setShrinkitMode(boolean shrinkitMode) {
|
||||
fileEntryReaderFn = this::handleShrinkitMode;
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleTextModeSetHighBit(FileEntryReader reader) {
|
||||
InputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
|
||||
return handleTextMode(TranslatorStream.builder(inputStream)
|
||||
.lfToCr().setHighBit().get(), reader);
|
||||
}
|
||||
private List<FileEntryReader> handleTextModeClearHighBit(FileEntryReader reader) {
|
||||
InputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
|
||||
return handleTextMode(TranslatorStream.builder(inputStream)
|
||||
.lfToCr().clearHighBit().get(), reader);
|
||||
}
|
||||
private List<FileEntryReader> handleTextMode(InputStream inputStream, FileEntryReader reader) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
StreamUtil.copy(inputStream, outputStream);
|
||||
final byte[] translatedData = outputStream.toByteArray();
|
||||
return OverrideFileEntryReader.builder()
|
||||
.fileData(translatedData)
|
||||
.prodosFiletype("TXT")
|
||||
.buildList(reader);
|
||||
} catch (IOException cause) {
|
||||
throw new UncheckedIOException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleDosMode(FileEntryReader reader) {
|
||||
try {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
|
||||
byte[] header = new byte[4];
|
||||
if (inputStream.read(header) != 4) {
|
||||
throw new IOException("Unable to read DOS header.");
|
||||
}
|
||||
int address = AppleUtil.getWordValue(header, 0);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(outputStream);
|
||||
return OverrideFileEntryReader.builder()
|
||||
.fileData(outputStream.toByteArray())
|
||||
.binaryAddress(address)
|
||||
.buildList(reader);
|
||||
} catch (IOException cause) {
|
||||
throw new UncheckedIOException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleGeosMode(FileEntryReader reader) {
|
||||
return OverrideFileEntryReader.builder()
|
||||
.prodosFiletype("GEO")
|
||||
.binaryAddress(0)
|
||||
.buildList(reader);
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleApplesoftTokenizeMode(FileEntryReader reader) {
|
||||
try {
|
||||
File fakeTempSource = File.createTempFile("ac-", "bas");
|
||||
fakeTempSource.deleteOnExit();
|
||||
Configuration config = Configuration.builder().sourceFile(fakeTempSource).build();
|
||||
Queue<Token> tokens = TokenReader.tokenize(new ByteArrayInputStream(reader.getFileData().get()));
|
||||
Parser parser = new Parser(tokens);
|
||||
Program program = parser.parse();
|
||||
byte[] tokenData = Visitors.byteVisitor(config).dump(program);
|
||||
return OverrideFileEntryReader.builder()
|
||||
.fileData(tokenData)
|
||||
.prodosFiletype("BAS")
|
||||
.binaryAddress(config.startAddress)
|
||||
.buildList(reader);
|
||||
} catch (IOException cause) {
|
||||
throw new UncheckedIOException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleAppleSingleMode(FileEntryReader reader) {
|
||||
try {
|
||||
AppleSingle as = AppleSingle.read(reader.getFileData().get());
|
||||
if (as.getProdosFileInfo() == null) {
|
||||
throw new IOException("This AppleSingle does not contain a ProDOS file.");
|
||||
}
|
||||
if (as.getDataFork() == null || as.getDataFork().length == 0) {
|
||||
throw new IOException("This AppleSingle does not contain a data fork.");
|
||||
}
|
||||
ProdosFileInfo info = as.getProdosFileInfo();
|
||||
String fileType = ProdosFormatDisk.getFiletype(info.getFileType());
|
||||
|
||||
OverrideFileEntryReader.Builder builder = OverrideFileEntryReader.builder();
|
||||
builder.filename(Optional.ofNullable(as.getRealName()));
|
||||
builder.fileData(as.getDataFork());
|
||||
builder.resourceData(Optional.ofNullable(as.getResourceFork()));
|
||||
builder.prodosFiletype(fileType);
|
||||
builder.locked(info.getAccess() == 0xc3);
|
||||
builder.auxiliaryType(info.getAuxType());
|
||||
|
||||
if (as.getFileDatesInfo() != null) {
|
||||
FileDatesInfo dates = as.getFileDatesInfo();
|
||||
builder.creationDate(Date.from(dates.getCreationInstant()));
|
||||
builder.lastModificationDate(Date.from(dates.getModificationInstant()));
|
||||
}
|
||||
|
||||
return builder.buildList(reader);
|
||||
} catch (IOException cause) {
|
||||
throw new UncheckedIOException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileEntryReader> handleShrinkitMode(FileEntryReader reader) {
|
||||
try {
|
||||
List<FileEntryReader> files = new ArrayList<>();
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(reader.getFileData().get());
|
||||
NuFileArchive nufx = new NuFileArchive(inputStream);
|
||||
for (HeaderBlock header : nufx.getHeaderBlocks()) {
|
||||
OverrideFileEntryReader.Builder builder = OverrideFileEntryReader.builder()
|
||||
.filename(header.getFilename())
|
||||
.prodosFiletype(ProdosFormatDisk.getFiletype((int)header.getFileType()))
|
||||
.auxiliaryType((int)header.getExtraType())
|
||||
.creationDate(header.getCreateWhen())
|
||||
.lastModificationDate(header.getModWhen());
|
||||
|
||||
ThreadRecord dataFork = header.getDataForkThreadRecord();
|
||||
ThreadRecord resourceFork = header.getResourceForkThreadRecord();
|
||||
if (dataFork == null) {
|
||||
LOG.info(() -> String.format("No data fork for '%s', skipping it.",
|
||||
header.getFilename()));
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.fileData(dataFork.getBytes());
|
||||
if (resourceFork != null) {
|
||||
builder.resourceData(resourceFork.getBytes());
|
||||
}
|
||||
files.add(builder.build());
|
||||
}
|
||||
return files;
|
||||
} catch (IOException cause) {
|
||||
throw new UncheckedIOException(cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Overrides {
|
||||
@Option(names = { "-t", "--type" }, description = "File type.")
|
||||
private Optional<String> fileType;
|
||||
|
||||
@Option(names = { "-a", "--addr" }, description = "File address.",
|
||||
converter = IntegerTypeConverter.class)
|
||||
private Optional<Integer> fileAddress;
|
||||
|
||||
@Option(names = { "-n", "--name" }, description = "File name.")
|
||||
private Optional<String> fileName;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk.DiskInformation;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
|
||||
@Command(name = "info", description = "Show information on a disk image(s).",
|
||||
aliases = "i")
|
||||
public class InfoCommand extends ReadOnlyDiskImageCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(InfoCommand.class.getName());
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
LOG.info(() -> "Path: " + disk.getFilename());
|
||||
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
|
||||
for (int i = 0; i < formattedDisks.length; i++) {
|
||||
FormattedDisk formattedDisk = formattedDisks[i];
|
||||
LOG.info(() -> String.format("Disk: %s (%s)", formattedDisk.getDiskName(), formattedDisk.getFormat()));
|
||||
for (DiskInformation diskinfo : formattedDisk.getDiskInformation()) {
|
||||
System.out.printf("%s: %s\n", diskinfo.getLabel(), diskinfo.getValue());
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk.FileColumnHeader;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadOnlyDiskImageCommandOptions;
|
||||
import io.github.applecommander.filestreamer.FileStreamer;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import io.github.applecommander.filestreamer.TypeOfFile;
|
||||
import picocli.CommandLine.ArgGroup;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
@Command(name = "list", description = "List directory of disk image(s).",
|
||||
aliases = { "ls" })
|
||||
public class ListCommand extends ReadOnlyDiskImageCommandOptions {
|
||||
@ArgGroup(exclusive = true, multiplicity = "0..1", heading = "%nFile display formatting:%n")
|
||||
private FileDisplay fileDisplay = new FileDisplay();
|
||||
|
||||
@Option(names = { "-r", "--recursive"}, description = "Display directory recursively.", negatable = true, defaultValue = "false")
|
||||
private boolean recursiveFlag;
|
||||
|
||||
@Option(names = { "--deleted" }, description = "Show deleted files.")
|
||||
private boolean deletedFlag;
|
||||
|
||||
@ArgGroup(exclusive = true, multiplicity = "0..1")
|
||||
private TypeOfFileSelection typeOfFile = new TypeOfFileSelection();
|
||||
|
||||
@Option(names = "--header", negatable = true, description = "Show header.")
|
||||
private boolean headerFlag = true;
|
||||
|
||||
@Option(names = "--column", negatable = true, description = "Show column headers.")
|
||||
private boolean columnFlag = true;
|
||||
|
||||
@Option(names = "--footer", negatable = true, description = "Show footer.")
|
||||
private boolean footerFlag = true;
|
||||
|
||||
@Option(names = "--globs", defaultValue = "*", split = ",", description = "File glob(s) to match.")
|
||||
private List<String> globs = new ArrayList<String>();
|
||||
|
||||
private List<String> fmtSpec;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
FileStreamer.forDisk(disk)
|
||||
.ignoreErrors(true)
|
||||
.includeDeleted(deletedFlag)
|
||||
.recursive(recursiveFlag)
|
||||
.includeTypeOfFile(typeOfFile.typeOfFile())
|
||||
.matchGlobs(globs)
|
||||
.beforeDisk(this::header)
|
||||
.afterDisk(this::footer)
|
||||
.stream()
|
||||
.forEach(this::list);
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void header(FormattedDisk disk) {
|
||||
List<FileColumnHeader> headers = disk.getFileColumnHeaders(fileDisplay.format());
|
||||
fmtSpec = createFormatSpec(headers);
|
||||
|
||||
System.out.println();
|
||||
System.out.printf("File: %s\n", disk.getFilename());
|
||||
System.out.printf("Name: %s\n", disk.getDiskName());
|
||||
}
|
||||
|
||||
protected void list(FileTuple tuple) {
|
||||
if (!deletedFlag && tuple.fileEntry.isDeleted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> data = tuple.fileEntry.getFileColumnData(fileDisplay.format());
|
||||
for (int i=0; i<tuple.paths.size(); i++) {
|
||||
System.out.print(" ");
|
||||
}
|
||||
for (int d = 0; d < data.size(); d++) {
|
||||
System.out.printf(fmtSpec.get(d), data.get(d));
|
||||
}
|
||||
if (tuple.fileEntry.isDeleted()) {
|
||||
System.out.print("[deleted]");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
protected void footer(FormattedDisk disk) {
|
||||
System.out.printf("%s format; %d bytes free; %d bytes used.\n",
|
||||
disk.getFormat(),
|
||||
disk.getFreeSpace(),
|
||||
disk.getUsedSpace());
|
||||
}
|
||||
|
||||
private List<String> createFormatSpec(List<FileColumnHeader> fileColumnHeaders) {
|
||||
List<String> fmtSpec = new ArrayList<>();
|
||||
for (FileColumnHeader h : fileColumnHeaders) {
|
||||
String spec = String.format("%%%s%ds ", h.isRightAlign() ? "" : "-",
|
||||
h.getMaximumWidth());
|
||||
fmtSpec.add(spec);
|
||||
}
|
||||
return fmtSpec;
|
||||
}
|
||||
|
||||
public static class FileDisplay {
|
||||
public int format() {
|
||||
if (standardFormat) {
|
||||
return FormattedDisk.FILE_DISPLAY_STANDARD;
|
||||
}
|
||||
if (longFormat) {
|
||||
return FormattedDisk.FILE_DISPLAY_DETAIL;
|
||||
}
|
||||
return FormattedDisk.FILE_DISPLAY_NATIVE;
|
||||
}
|
||||
|
||||
@Option(names = { "-n", "--native" }, description = "Use native directory format (default).")
|
||||
private boolean nativeFormat;
|
||||
|
||||
@Option(names = { "-s", "--short", "--standard" }, description = "Use brief directory format.")
|
||||
private boolean standardFormat;
|
||||
|
||||
@Option(names = { "-l", "--long", "--detail" }, description = "Use long/detailed directory format.")
|
||||
private boolean longFormat;
|
||||
}
|
||||
|
||||
public static class TypeOfFileSelection {
|
||||
public TypeOfFile typeOfFile() {
|
||||
if (filesOnly) {
|
||||
return TypeOfFile.FILE;
|
||||
}
|
||||
if (directoriesOnly) {
|
||||
return TypeOfFile.DIRECTORY;
|
||||
}
|
||||
return TypeOfFile.BOTH;
|
||||
}
|
||||
|
||||
@Option(names = "--file", description = "Only include files.")
|
||||
private boolean filesOnly;
|
||||
|
||||
@Option(names = "--directory", description = "Only include directories.")
|
||||
private boolean directoriesOnly;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import picocli.CommandLine.Command;
|
||||
|
||||
@Command(name = "lock", description = "Lock file(s) on a disk image.")
|
||||
public class LockCommand extends ReadWriteDiskCommandWithGlobOptions {
|
||||
private static Logger LOG = Logger.getLogger(LockCommand.class.getName());
|
||||
|
||||
public void fileHandler(FileTuple tuple) {
|
||||
tuple.fileEntry.setLocked(true);
|
||||
LOG.info(() -> String.format("File '%s' locked.", tuple.fileEntry.getFilename()));
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "mkdir", description = "Create a directory on disk.",
|
||||
aliases = { "md" })
|
||||
public class MkdirCommand extends ReadWriteDiskCommandOptions {
|
||||
@Option(names = { "-p" }, description = "Create intermediate subdirectories.")
|
||||
private boolean prefixFlag;
|
||||
|
||||
@Parameters(description = "Directory name to create (use '/' as divider).")
|
||||
private String fullPath;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
|
||||
DirectoryEntry directory = formattedDisk;
|
||||
|
||||
String[] paths = fullPath.split("/");
|
||||
for (int i=0; i<paths.length; i++) {
|
||||
final String pathName = formattedDisk.getSuggestedFilename(paths[i]);
|
||||
Optional<FileEntry> optEntry = directory.getFiles().stream()
|
||||
.filter(entry -> entry.getFilename().equalsIgnoreCase(pathName))
|
||||
.findFirst();
|
||||
|
||||
if (optEntry.isPresent()) {
|
||||
FileEntry fileEntry = optEntry.get();
|
||||
if (fileEntry instanceof DirectoryEntry) {
|
||||
directory = (DirectoryEntry)fileEntry;
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException(String.format("Not a directory: '%s'", pathName));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (prefixFlag || i == paths.length-1) {
|
||||
directory = directory.createDirectory(pathName);
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException(String.format("Directory does not exist: '%s'", pathName));
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
import com.webcodepro.applecommander.storage.os.pascal.PascalFormatDisk;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFormatDisk;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "rename-disk", description = "Rename volume of a disk image.")
|
||||
public class RenameDiskCommand extends ReadWriteDiskCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(RenameDiskCommand.class.getName());
|
||||
|
||||
@Parameters(description = "Disk name.")
|
||||
private String diskName;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
FormattedDisk[] formattedDisks = disk.getFormattedDisks();
|
||||
FormattedDisk formattedDisk = formattedDisks[0];
|
||||
if (formattedDisk instanceof ProdosFormatDisk || formattedDisk instanceof PascalFormatDisk) {
|
||||
formattedDisk.setDiskName(diskName);
|
||||
return 0;
|
||||
} else {
|
||||
LOG.warning("Disk must be ProDOS or Pascal.");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import io.github.applecommander.filestreamer.FileStreamer;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import io.github.applecommander.filestreamer.TypeOfFile;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "rename", description = "Rename file on a disk image.",
|
||||
aliases = { "ren" })
|
||||
public class RenameFileCommand extends ReadWriteDiskCommandOptions {
|
||||
private static Logger LOG = Logger.getLogger(RenameFileCommand.class.getName());
|
||||
|
||||
@Option(names = { "-m", "--multiple" }, description = "Force rename when multiple files found.")
|
||||
private boolean multipleOverride;
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Rename locked files.")
|
||||
private boolean lockOverride;
|
||||
|
||||
@Parameters(index = "0", description = "Original file name (include path).")
|
||||
private String originalFilename;
|
||||
|
||||
@Parameters(index = "1", description = "New file name (just the new filename).")
|
||||
private String newFilename;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
List<FileTuple> files = FileStreamer.forDisk(disk)
|
||||
.ignoreErrors(true)
|
||||
.includeTypeOfFile(TypeOfFile.FILE)
|
||||
.matchGlobs(originalFilename)
|
||||
.stream()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (files.isEmpty()) {
|
||||
LOG.warning(() -> String.format("File not found for %s.", originalFilename));
|
||||
}
|
||||
else if (!multipleOverride && files.size() > 1) {
|
||||
LOG.severe(() -> String.format("Multile files with %s found (count = %d).",
|
||||
originalFilename, files.size()));
|
||||
}
|
||||
else {
|
||||
files.forEach(this::fileHandler);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void fileHandler(FileTuple tuple) {
|
||||
if (tuple.fileEntry.isLocked()) {
|
||||
if (lockOverride) {
|
||||
LOG.info(() -> String.format("File '%s' is locked, but 'force' specified; ignoring lock.",
|
||||
tuple.fileEntry.getFilename()));
|
||||
} else {
|
||||
LOG.warning(() -> String.format("File '%s' is locked.", tuple.fileEntry.getFilename()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
tuple.fileEntry.setFilename(newFilename);
|
||||
LOG.info(() -> String.format("File '%s' renamed to '%s'.", originalFilename, newFilename));
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandOptions;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Parameters;
|
||||
|
||||
@Command(name = "rmdir", description = "Remove a directory on disk.",
|
||||
aliases = { "rd" })
|
||||
public class RmdirCommand extends ReadWriteDiskCommandOptions {
|
||||
@Option(names = { "-r", "--recursive" }, description = "Recursively delete subdirectories.")
|
||||
private boolean recursiveFlag;
|
||||
|
||||
@Option(names = { "-f", "--force" }, description = "Force files to be deleted as well.")
|
||||
private boolean forceFlag;
|
||||
|
||||
@Parameters(description = "Directory name to delete (use '/' as divider).")
|
||||
private String fullPath;
|
||||
|
||||
@Override
|
||||
public int handleCommand() throws Exception {
|
||||
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
|
||||
|
||||
// Locate directory
|
||||
DirectoryEntry directory = formattedDisk;
|
||||
String[] paths = fullPath.split("/");
|
||||
for (int i=0; i<paths.length; i++) {
|
||||
final String pathName = formattedDisk.getSuggestedFilename(paths[i]);
|
||||
Optional<FileEntry> optEntry = directory.getFiles().stream()
|
||||
.filter(entry -> entry.getFilename().equalsIgnoreCase(pathName))
|
||||
.findFirst();
|
||||
|
||||
if (optEntry.isPresent()) {
|
||||
FileEntry fileEntry = optEntry.get();
|
||||
if (fileEntry instanceof DirectoryEntry) {
|
||||
directory = (DirectoryEntry)fileEntry;
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException(String.format("Not a directory: '%s'", pathName));
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new RuntimeException(String.format("Directory does not exist: '%s'", pathName));
|
||||
}
|
||||
}
|
||||
|
||||
deleteDirectory(directory);
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void deleteDirectory(DirectoryEntry directory) throws DiskException {
|
||||
for (FileEntry file : directory.getFiles()) {
|
||||
if (file.isDeleted()) {
|
||||
// skip
|
||||
} else if (recursiveFlag && file.isDirectory()) {
|
||||
deleteDirectory((DirectoryEntry)file);
|
||||
}
|
||||
else if (forceFlag && !file.isDirectory()) {
|
||||
file.delete();
|
||||
}
|
||||
else {
|
||||
String message = String.format("Encountered %s '%s'",
|
||||
file.isDirectory() ? "directory" : "file",
|
||||
file.getFilename());
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
}
|
||||
|
||||
FileEntry file = (FileEntry)directory;
|
||||
file.delete();
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package io.github.applecommander.acx.command;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import io.github.applecommander.acx.base.ReadWriteDiskCommandWithGlobOptions;
|
||||
import io.github.applecommander.filestreamer.FileTuple;
|
||||
import picocli.CommandLine.Command;
|
||||
|
||||
@Command(name = "unlock", description = "Unlock file(s) on a disk image.")
|
||||
public class UnlockCommand extends ReadWriteDiskCommandWithGlobOptions {
|
||||
private static Logger LOG = Logger.getLogger(UnlockCommand.class.getName());
|
||||
|
||||
public void fileHandler(FileTuple tuple) {
|
||||
tuple.fileEntry.setLocked(false);
|
||||
LOG.info(() -> String.format("File '%s' unlocked.", tuple.fileEntry.getFilename()));
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package io.github.applecommander.acx.converter;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import picocli.CommandLine.ITypeConverter;
|
||||
import picocli.CommandLine.TypeConversionException;
|
||||
|
||||
public class DataSizeConverter implements ITypeConverter<Integer> {
|
||||
public static final int KB = 1024;
|
||||
public static final int MB = KB * 1024;
|
||||
|
||||
@Override
|
||||
public Integer convert(String value) throws Exception {
|
||||
Pattern pattern = Pattern.compile("([0-9]+)([km]b?)?", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(value);
|
||||
if (matcher.matches()) {
|
||||
String number = matcher.group(1);
|
||||
String kmb = matcher.group(2);
|
||||
if (kmb != null) {
|
||||
kmb = kmb.toLowerCase();
|
||||
}
|
||||
int bytes = Integer.parseInt(number);
|
||||
if (kmb.startsWith("k")) {
|
||||
bytes *= KB;
|
||||
}
|
||||
else if (kmb.startsWith("m")) {
|
||||
bytes *= MB;
|
||||
}
|
||||
else {
|
||||
throw new TypeConversionException(String.format("Unexpected data size '%s'", kmb));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
throw new TypeConversionException("Expecting format like '140kb' or '5mb'");
|
||||
}
|
||||
|
||||
public static String format(int value) {
|
||||
if (value < KB) {
|
||||
return String.format("%,dB", value);
|
||||
}
|
||||
if (value < MB) {
|
||||
return String.format("%,dKB", value / KB);
|
||||
}
|
||||
return String.format("%,dMB", value / MB);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package io.github.applecommander.acx.converter;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
|
||||
import picocli.CommandLine.ITypeConverter;
|
||||
import picocli.CommandLine.TypeConversionException;
|
||||
|
||||
public class DiskConverter implements ITypeConverter<Disk> {
|
||||
@Override
|
||||
public Disk convert(String filename) throws Exception {
|
||||
if (Files.exists(Path.of(filename))) {
|
||||
return new Disk(filename);
|
||||
}
|
||||
throw new TypeConversionException(String.format("Disk '%s' not found", filename));
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package io.github.applecommander.acx.converter;
|
||||
|
||||
import picocli.CommandLine.ITypeConverter;
|
||||
|
||||
/** Add support for "$801" and "0x801" instead of just decimal like 2049. */
|
||||
public class IntegerTypeConverter implements ITypeConverter<Integer> {
|
||||
@Override
|
||||
public Integer convert(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
} else if (value.startsWith("$")) {
|
||||
return Integer.valueOf(value.substring(1), 16);
|
||||
} else if (value.startsWith("0x") || value.startsWith("0X")) {
|
||||
return Integer.valueOf(value.substring(2), 16);
|
||||
} else {
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package io.github.applecommander.acx.converter;
|
||||
|
||||
import io.github.applecommander.acx.SystemType;
|
||||
import picocli.CommandLine.ITypeConverter;
|
||||
|
||||
public class SystemTypeConverter implements ITypeConverter<SystemType> {
|
||||
@Override
|
||||
public SystemType convert(String value) throws Exception {
|
||||
return SystemType.valueOf(value.replaceAll("[^a-zA-Z]", "").toUpperCase());
|
||||
}
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DiskFullException;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFormatDisk;
|
||||
import com.webcodepro.applecommander.util.AppleUtil;
|
||||
|
||||
public class DosFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
|
||||
private static final Map<String,String> FILE_TYPES;
|
||||
static {
|
||||
FILE_TYPES = Map.of(
|
||||
"T", "TXT",
|
||||
"I", "INT",
|
||||
"A", "BAS",
|
||||
"B", "BIN",
|
||||
"S", "$F1",
|
||||
"R", "REL",
|
||||
"a", "$F2",
|
||||
"b", "$F3"
|
||||
);
|
||||
}
|
||||
|
||||
private DosFileEntry fileEntry;
|
||||
|
||||
public DosFileEntryReaderWriter(DosFileEntry fileEntry) {
|
||||
if (fileEntry.isDeleted()) {
|
||||
throw new RuntimeException("Unable to copy deleted files.");
|
||||
}
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.of(fileEntry.getFilename());
|
||||
}
|
||||
@Override
|
||||
public void setFilename(String filename) {
|
||||
fileEntry.setFilename(filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getProdosFiletype() {
|
||||
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
|
||||
}
|
||||
@Override
|
||||
public void setProdosFiletype(String filetype) {
|
||||
String dosFileType = FILE_TYPES.entrySet()
|
||||
.stream()
|
||||
.filter(e -> e.getValue().equals(filetype))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst()
|
||||
.orElse("B");
|
||||
fileEntry.setFiletype(dosFileType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> isLocked() {
|
||||
return Optional.of(fileEntry.isLocked());
|
||||
}
|
||||
@Override
|
||||
public void setLocked(boolean flag) {
|
||||
fileEntry.setLocked(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
return Optional.ofNullable(fileEntry.getFileData());
|
||||
}
|
||||
@Override
|
||||
public void setFileData(byte[] data) {
|
||||
try {
|
||||
fileEntry.setFileData(data);
|
||||
} catch (DiskFullException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getBinaryAddress() {
|
||||
if (fileEntry.isBinaryFile()) {
|
||||
byte[] rawdata = getRawFileData();
|
||||
return Optional.of(AppleUtil.getWordValue(rawdata, 0));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
@Override
|
||||
public void setBinaryAddress(int address) {
|
||||
if (fileEntry.needsAddress()) {
|
||||
fileEntry.setAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getBinaryLength() {
|
||||
if (fileEntry.isBinaryFile() || fileEntry.isApplesoftBasicFile() || fileEntry.isIntegerBasicFile()) {
|
||||
// DosFileEntry pulls the address
|
||||
return Optional.of(fileEntry.getSize());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
@Override
|
||||
public void setBinaryLength(int length) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
private byte[] getRawFileData() {
|
||||
DosFormatDisk disk = (DosFormatDisk) fileEntry.getFormattedDisk();
|
||||
return disk.getFileData(fileEntry);
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.nakedos.NakedosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.rdos.RdosFileEntry;
|
||||
|
||||
public interface FileEntryReader {
|
||||
// FileEntry common
|
||||
public default Optional<String> getFilename() { return Optional.empty(); }
|
||||
public default Optional<String> getProdosFiletype() { return Optional.empty(); }
|
||||
public default Optional<Boolean> isLocked() { return Optional.empty(); }
|
||||
public default Optional<byte[]> getFileData() { return Optional.empty(); }
|
||||
public default Optional<byte[]> getResourceData() { return Optional.empty(); }
|
||||
/**
|
||||
* The address embedded in binary objects.
|
||||
* This varies by DOS's so is split apart.
|
||||
*/
|
||||
public default Optional<Integer> getBinaryAddress() { return Optional.empty(); }
|
||||
/**
|
||||
* The length embedded in binary, Applesoft, Integer BASIC objects.
|
||||
* This varies by DOS's so is split apart.
|
||||
*/
|
||||
public default Optional<Integer> getBinaryLength() { return Optional.empty(); }
|
||||
// ProdosFileEntry specific
|
||||
public default Optional<Integer> getAuxiliaryType() { return Optional.empty(); }
|
||||
public default Optional<Date> getCreationDate() { return Optional.empty(); }
|
||||
// ProdosFileEntry / PascalFileEntry specific
|
||||
public default Optional<Date> getLastModificationDate() { return Optional.empty(); }
|
||||
|
||||
public static FileEntryReader get(FileEntry fileEntry) {
|
||||
if (fileEntry instanceof DosFileEntry) {
|
||||
return new DosFileEntryReaderWriter((DosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof NakedosFileEntry) {
|
||||
return new NakedosFileEntryReader((NakedosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof PascalFileEntry) {
|
||||
return new PascalFileEntryReaderWriter((PascalFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof ProdosFileEntry) {
|
||||
return new ProdosFileEntryReaderWriter((ProdosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof RdosFileEntry) {
|
||||
return new RdosFileEntryReader((RdosFileEntry)fileEntry);
|
||||
}
|
||||
throw new RuntimeException(String.format("No reader for %s", fileEntry.getClass().getName()));
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
|
||||
|
||||
public interface FileEntryWriter {
|
||||
// FileEntry common
|
||||
public default void setFilename(String filename) { }
|
||||
public default void setProdosFiletype(String filetype) { }
|
||||
public default void setLocked(boolean flag) { }
|
||||
public default void setFileData(byte[] data) { }
|
||||
// Special case for GS/OS files (uglifies API; sets 0x05)
|
||||
public default void setFileData(byte[] data, byte[] resource) { }
|
||||
/**
|
||||
* The address embedded in binary objects.
|
||||
* This varies by DOS's so is split apart.
|
||||
*/
|
||||
public default void setBinaryAddress(int address) { }
|
||||
/**
|
||||
* The length embedded in binary, Applesoft, Integer BASIC objects.
|
||||
* This varies by DOS's so is split apart.
|
||||
*/
|
||||
public default void setBinaryLength(int length) { }
|
||||
// ProdosFileEntry specific
|
||||
public default void setAuxiliaryType(int auxType) { }
|
||||
public default void setCreationDate(Date date) { }
|
||||
// ProdosFileEntry / PascalFileEntry specific
|
||||
public default void setLastModificationDate(Date date) { }
|
||||
|
||||
public static FileEntryWriter get(FileEntry fileEntry) {
|
||||
if (fileEntry instanceof DosFileEntry) {
|
||||
return new DosFileEntryReaderWriter((DosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof ProdosFileEntry) {
|
||||
return new ProdosFileEntryReaderWriter((ProdosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof PascalFileEntry) {
|
||||
return new PascalFileEntryReaderWriter((PascalFileEntry)fileEntry);
|
||||
}
|
||||
throw new RuntimeException(String.format("No writer for %s", fileEntry.getClass().getName()));
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
|
||||
import io.github.applecommander.acx.command.CopyFileCommand;
|
||||
|
||||
public class FileUtils {
|
||||
private static Logger LOG = Logger.getLogger(CopyFileCommand.class.getName());
|
||||
|
||||
private boolean overwrite;
|
||||
|
||||
public FileUtils(boolean overwrite) {
|
||||
this.overwrite = overwrite;
|
||||
}
|
||||
|
||||
public void copy(DirectoryEntry directory, FileEntry file) throws DiskException {
|
||||
LOG.fine(() -> String.format("Copying '%s'", file.getFilename()));
|
||||
if (file.isDeleted()) {
|
||||
// Skip deleted files
|
||||
}
|
||||
else if (file.isDirectory()) {
|
||||
copyDirectory(directory, (DirectoryEntry)file, file.getFilename());
|
||||
}
|
||||
else {
|
||||
copyFile(directory, file);
|
||||
}
|
||||
}
|
||||
|
||||
void copyDirectory(DirectoryEntry targetParent, DirectoryEntry sourceDir, String name) throws DiskException {
|
||||
Optional<FileEntry> targetFile = targetParent.getFiles()
|
||||
.stream()
|
||||
.filter(fileEntry -> name.equals(fileEntry.getFilename()))
|
||||
.findFirst();
|
||||
Optional<DirectoryEntry> targetDir = targetFile
|
||||
.filter(FileEntry::isDirectory)
|
||||
.map(DirectoryEntry.class::cast);
|
||||
|
||||
if (targetDir.isPresent()) {
|
||||
// Fall through to general logic
|
||||
}
|
||||
else if (targetFile.isPresent()) {
|
||||
// This is an abstract class, so faking it for now.
|
||||
throw new DiskException("Unable to create directory", name) {
|
||||
private static final long serialVersionUID = 4726414295404986677L;
|
||||
};
|
||||
}
|
||||
else {
|
||||
targetDir = Optional.of(targetParent.createDirectory(name));
|
||||
}
|
||||
|
||||
for (FileEntry fileEntry : sourceDir.getFiles()) {
|
||||
copy(targetDir.get(), fileEntry);
|
||||
}
|
||||
}
|
||||
|
||||
void copyFile(DirectoryEntry directory, FileEntry sourceFile) throws DiskException {
|
||||
FileEntryReader source = FileEntryReader.get(sourceFile);
|
||||
copyFile(directory, source);
|
||||
}
|
||||
|
||||
public void copyFile(DirectoryEntry directory, FileEntryReader source) throws DiskException {
|
||||
String sourceName = source.getFilename().get();
|
||||
String sanitizedName = directory.getFormattedDisk().getSuggestedFilename(sourceName);
|
||||
final Optional<FileEntry> fileEntry = directory.getFiles().stream()
|
||||
.filter(entry -> entry.getFilename().equals(sanitizedName))
|
||||
.findFirst();
|
||||
|
||||
final FileEntry targetFile;
|
||||
if (fileEntry.isPresent()) {
|
||||
targetFile = fileEntry
|
||||
.filter(entry -> overwrite)
|
||||
.orElseThrow(() -> new RuntimeException(String.format("File '%s' exists.",
|
||||
source.getFilename().get())));
|
||||
}
|
||||
else {
|
||||
targetFile = directory.createFile();
|
||||
}
|
||||
|
||||
FileEntryWriter target = FileEntryWriter.get(targetFile);
|
||||
|
||||
source.getFilename().ifPresent(target::setFilename);
|
||||
source.getProdosFiletype().ifPresent(target::setProdosFiletype);
|
||||
source.isLocked().ifPresent(target::setLocked);
|
||||
source.getBinaryAddress().ifPresent(target::setBinaryAddress);
|
||||
source.getBinaryLength().ifPresent(target::setBinaryLength);
|
||||
source.getAuxiliaryType().ifPresent(target::setAuxiliaryType);
|
||||
source.getCreationDate().ifPresent(target::setCreationDate);
|
||||
source.getLastModificationDate().ifPresent(target::setLastModificationDate);
|
||||
|
||||
if (source.getFileData().isPresent() && source.getResourceData().isPresent()) {
|
||||
target.setFileData(source.getFileData().get(), source.getResourceData().get());
|
||||
} else {
|
||||
source.getFileData().ifPresent(target::setFileData);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.os.nakedos.NakedosFileEntry;
|
||||
|
||||
public class NakedosFileEntryReader implements FileEntryReader {
|
||||
private NakedosFileEntry fileEntry;
|
||||
|
||||
public NakedosFileEntryReader(NakedosFileEntry fileEntry) {
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.of(fileEntry.getFilename());
|
||||
}
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
return Optional.of(fileEntry.getFileData());
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Allow programmatic control of what is in the results of the file entry.
|
||||
* Useful for translating from raw data.
|
||||
* It can also be used to layer in results and overrides.
|
||||
*/
|
||||
public class OverrideFileEntryReader implements FileEntryReader {
|
||||
private Optional<FileEntryReader> parent = Optional.empty();
|
||||
private Optional<String> filename = Optional.empty();
|
||||
private Optional<String> prodosFiletype = Optional.empty();
|
||||
private Optional<Boolean> locked = Optional.empty();
|
||||
private Optional<byte[]> fileData = Optional.empty();
|
||||
private Optional<byte[]> resourceData = Optional.empty();
|
||||
private Optional<Integer> binaryAddress = Optional.empty();
|
||||
private Optional<Integer> binaryLength = Optional.empty();
|
||||
private Optional<Integer> auxiliaryType = Optional.empty();
|
||||
private Optional<Date> creationDate = Optional.empty();
|
||||
private Optional<Date> lastModificationDate = Optional.empty();
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return filename.or(() -> parent.map(FileEntryReader::getFilename).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<String> getProdosFiletype() {
|
||||
return prodosFiletype.or(() -> parent.map(FileEntryReader::getProdosFiletype).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<Boolean> isLocked() {
|
||||
return locked.or(() -> parent.map(FileEntryReader::isLocked).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
return fileData.or(() -> parent.map(FileEntryReader::getFileData).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<byte[]> getResourceData() {
|
||||
// Special case, the AppleCommander API does not really handle resource forks.
|
||||
return resourceData;
|
||||
}
|
||||
@Override
|
||||
public Optional<Integer> getBinaryAddress() {
|
||||
return binaryAddress.or(() -> parent.map(FileEntryReader::getBinaryAddress).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<Integer> getBinaryLength() {
|
||||
return binaryLength.or(() -> parent.map(FileEntryReader::getBinaryLength).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<Integer> getAuxiliaryType() {
|
||||
return auxiliaryType.or(() -> parent.map(FileEntryReader::getBinaryLength).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<Date> getCreationDate() {
|
||||
return creationDate.or(() -> parent.map(FileEntryReader::getCreationDate).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
@Override
|
||||
public Optional<Date> getLastModificationDate() {
|
||||
return lastModificationDate.or(() -> parent.map(FileEntryReader::getLastModificationDate).filter(Optional::isPresent).map(Optional::get));
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private OverrideFileEntryReader fileEntryReader = new OverrideFileEntryReader();
|
||||
|
||||
public FileEntryReader build(FileEntryReader parent) {
|
||||
Objects.requireNonNull(parent);
|
||||
fileEntryReader.parent = Optional.of(parent);
|
||||
return fileEntryReader;
|
||||
}
|
||||
public List<FileEntryReader> buildList(FileEntryReader parent) {
|
||||
return Arrays.asList(build(parent));
|
||||
}
|
||||
public FileEntryReader build() {
|
||||
return fileEntryReader;
|
||||
}
|
||||
public List<FileEntryReader> buildList() {
|
||||
return Arrays.asList(build());
|
||||
}
|
||||
|
||||
public Builder filename(String filename) {
|
||||
Objects.requireNonNull(filename);
|
||||
fileEntryReader.filename = Optional.of(filename);
|
||||
return this;
|
||||
}
|
||||
public Builder filename(Optional<String> filename) {
|
||||
Objects.requireNonNull(filename);
|
||||
fileEntryReader.filename = filename;
|
||||
return this;
|
||||
}
|
||||
public Builder prodosFiletype(String filetype) {
|
||||
Objects.requireNonNull(filetype);
|
||||
fileEntryReader.prodosFiletype = Optional.of(filetype);
|
||||
return this;
|
||||
}
|
||||
public Builder prodosFiletype(Optional<String> filetype) {
|
||||
Objects.requireNonNull(filetype);
|
||||
fileEntryReader.prodosFiletype = filetype;
|
||||
return this;
|
||||
}
|
||||
public Builder locked(boolean locked) {
|
||||
fileEntryReader.locked = Optional.of(locked);
|
||||
return this;
|
||||
}
|
||||
public Builder locked(Optional<Boolean> locked) {
|
||||
Objects.requireNonNull(locked);
|
||||
fileEntryReader.locked = locked;
|
||||
return this;
|
||||
}
|
||||
public Builder fileData(byte[] fileData) {
|
||||
Objects.requireNonNull(fileData);
|
||||
fileEntryReader.fileData = Optional.of(fileData);
|
||||
return this;
|
||||
}
|
||||
public Builder fileData(Optional<byte[]> fileData) {
|
||||
Objects.requireNonNull(fileData);
|
||||
fileEntryReader.fileData = fileData;
|
||||
return this;
|
||||
}
|
||||
public Builder resourceData(byte[] resourceData) {
|
||||
Objects.requireNonNull(resourceData);
|
||||
fileEntryReader.resourceData = Optional.of(resourceData);
|
||||
return this;
|
||||
}
|
||||
public Builder resourceData(Optional<byte[]> resourceData) {
|
||||
Objects.requireNonNull(resourceData);
|
||||
fileEntryReader.resourceData = resourceData;
|
||||
return this;
|
||||
}
|
||||
public Builder binaryAddress(int binaryAddress) {
|
||||
fileEntryReader.binaryAddress = Optional.of(binaryAddress);
|
||||
return this;
|
||||
}
|
||||
public Builder binaryAddress(Optional<Integer> binaryAddress) {
|
||||
Objects.requireNonNull(binaryAddress);
|
||||
fileEntryReader.binaryAddress = binaryAddress;
|
||||
return this;
|
||||
}
|
||||
public Builder binaryLength(int binaryLength) {
|
||||
fileEntryReader.binaryLength = Optional.of(binaryLength);
|
||||
return this;
|
||||
}
|
||||
public Builder binaryLength(Optional<Integer> binaryLength) {
|
||||
Objects.requireNonNull(binaryLength);
|
||||
fileEntryReader.binaryLength = binaryLength;
|
||||
return this;
|
||||
}
|
||||
public Builder auxiliaryType(int auxiliaryType) {
|
||||
fileEntryReader.auxiliaryType = Optional.of(auxiliaryType);
|
||||
return this;
|
||||
}
|
||||
public Builder auxiliaryType(Optional<Integer> auxiliaryType) {
|
||||
Objects.requireNonNull(auxiliaryType);
|
||||
fileEntryReader.auxiliaryType = auxiliaryType;
|
||||
return this;
|
||||
}
|
||||
public Builder creationDate(Date creationDate) {
|
||||
Objects.requireNonNull(creationDate);
|
||||
fileEntryReader.creationDate = Optional.of(creationDate);
|
||||
return this;
|
||||
}
|
||||
public Builder creationDate(Optional<Date> creationDate) {
|
||||
Objects.requireNonNull(creationDate);
|
||||
fileEntryReader.creationDate = creationDate;
|
||||
return this;
|
||||
}
|
||||
public Builder lastModificationDate(Date lastModificationDate) {
|
||||
Objects.requireNonNull(lastModificationDate);
|
||||
fileEntryReader.lastModificationDate = Optional.of(lastModificationDate);
|
||||
return this;
|
||||
}
|
||||
public Builder lastModificationDate(Optional<Date> lastModificationDate) {
|
||||
Objects.requireNonNull(lastModificationDate);
|
||||
fileEntryReader.lastModificationDate = lastModificationDate;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DiskFullException;
|
||||
import com.webcodepro.applecommander.storage.filters.PascalTextFileFilter;
|
||||
import com.webcodepro.applecommander.storage.os.pascal.PascalFileEntry;
|
||||
|
||||
public class PascalFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
|
||||
private static final PascalTextFileFilter TEXT_FILTER = new PascalTextFileFilter();
|
||||
private static final Map<String,String> FILE_TYPES;
|
||||
static {
|
||||
FILE_TYPES = Map.of(
|
||||
// Pascal => Prodos
|
||||
"xdskfile", "BAD", // TODO we should skip bad block files
|
||||
"CODE", "BIN", // TODO is there an address?
|
||||
"TEXT", "TXT",
|
||||
"INFO", "TXT", // TODO We should skip debugger info
|
||||
"DATA", "BIN",
|
||||
"GRAF", "BIN", // TODO compressed graphics image
|
||||
"FOTO", "BIN", // TODO screen image
|
||||
"securedir", "BIN", // TODO is this even implemented
|
||||
|
||||
// Prodos => Pascal
|
||||
"BIN", "DATA",
|
||||
"TXT", "TEXT"
|
||||
);
|
||||
}
|
||||
|
||||
private PascalFileEntry fileEntry;
|
||||
|
||||
public PascalFileEntryReaderWriter(PascalFileEntry fileEntry) {
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.ofNullable(fileEntry.getFilename());
|
||||
}
|
||||
@Override
|
||||
public void setFilename(String filename) {
|
||||
fileEntry.setFilename(filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getProdosFiletype() {
|
||||
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
|
||||
}
|
||||
@Override
|
||||
public void setProdosFiletype(String filetype) {
|
||||
fileEntry.setFiletype(FILE_TYPES.getOrDefault(filetype, "DATA"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Date> getLastModificationDate() {
|
||||
return Optional.ofNullable(fileEntry.getModificationDate());
|
||||
}
|
||||
@Override
|
||||
public void setLastModificationDate(Date date) {
|
||||
fileEntry.setModificationDate(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
if ("TEXT".equals(fileEntry.getFiletype())) {
|
||||
return Optional.ofNullable(TEXT_FILTER.filter(fileEntry));
|
||||
}
|
||||
else {
|
||||
return Optional.ofNullable(fileEntry.getFileData());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setFileData(byte[] data) {
|
||||
try {
|
||||
fileEntry.setFileData(data);
|
||||
} catch (DiskFullException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DiskFullException;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
|
||||
|
||||
public class ProdosFileEntryReaderWriter implements FileEntryReader, FileEntryWriter {
|
||||
private ProdosFileEntry fileEntry;
|
||||
|
||||
public ProdosFileEntryReaderWriter(ProdosFileEntry fileEntry) {
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.ofNullable(fileEntry.getFilename());
|
||||
}
|
||||
@Override
|
||||
public void setFilename(String filename) {
|
||||
fileEntry.setFilename(filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getProdosFiletype() {
|
||||
return Optional.ofNullable(fileEntry.getFiletype());
|
||||
}
|
||||
@Override
|
||||
public void setProdosFiletype(String filetype) {
|
||||
fileEntry.setFiletype(filetype);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Boolean> isLocked() {
|
||||
return Optional.ofNullable(fileEntry.isLocked());
|
||||
}
|
||||
@Override
|
||||
public void setLocked(boolean flag) {
|
||||
fileEntry.setLocked(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
return Optional.ofNullable(fileEntry.getFileData());
|
||||
}
|
||||
@Override
|
||||
public void setFileData(byte[] data) {
|
||||
try {
|
||||
fileEntry.setFileData(data);
|
||||
} catch (DiskFullException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setFileData(byte[] data, byte[] resource) {
|
||||
try {
|
||||
// If we have a resource fork in addition to a data fork,
|
||||
// then we've got a GSOS storage type $5.
|
||||
fileEntry.setFileData(data, resource);
|
||||
fileEntry.setStorageType(0x05);
|
||||
} catch (DiskFullException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getBinaryAddress() {
|
||||
if (fileEntry.needsAddress()) {
|
||||
return Optional.ofNullable(fileEntry.getAuxiliaryType());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
@Override
|
||||
public void setBinaryAddress(int address) {
|
||||
if (fileEntry.needsAddress()) {
|
||||
fileEntry.setAddress(address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getBinaryLength() {
|
||||
return Optional.ofNullable(fileEntry.getSize());
|
||||
}
|
||||
@Override
|
||||
public void setBinaryLength(int length) {
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getAuxiliaryType() {
|
||||
return Optional.ofNullable(fileEntry.getAuxiliaryType());
|
||||
}
|
||||
@Override
|
||||
public void setAuxiliaryType(int auxType) {
|
||||
fileEntry.setAuxiliaryType(auxType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Date> getCreationDate() {
|
||||
return Optional.ofNullable(fileEntry.getCreationDate());
|
||||
}
|
||||
@Override
|
||||
public void setCreationDate(Date date) {
|
||||
fileEntry.setCreationDate(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Date> getLastModificationDate() {
|
||||
return Optional.ofNullable(fileEntry.getLastModificationDate());
|
||||
}
|
||||
@Override
|
||||
public void setLastModificationDate(Date date) {
|
||||
fileEntry.setLastModificationDate(date);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package io.github.applecommander.acx.fileutil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.webcodepro.applecommander.storage.os.rdos.RdosFileEntry;
|
||||
|
||||
public class RdosFileEntryReader implements FileEntryReader {
|
||||
private static final Map<String,String> FILE_TYPES;
|
||||
static {
|
||||
FILE_TYPES = Map.of(
|
||||
"T", "TXT",
|
||||
"A", "BAS",
|
||||
"B", "BIN"
|
||||
);
|
||||
}
|
||||
|
||||
private RdosFileEntry fileEntry;
|
||||
|
||||
public RdosFileEntryReader(RdosFileEntry fileEntry) {
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getFilename() {
|
||||
return Optional.ofNullable(fileEntry.getFilename());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Integer> getBinaryAddress() {
|
||||
return Optional.ofNullable(fileEntry.getAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getProdosFiletype() {
|
||||
return Optional.ofNullable(FILE_TYPES.get(fileEntry.getFiletype()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<byte[]> getFileData() {
|
||||
return Optional.ofNullable(fileEntry.getFileData());
|
||||
}
|
||||
}
|
@ -0,0 +1,205 @@
|
||||
package io.github.applecommander.filestreamer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Spliterators;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.DiskUnrecognizedException;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
/**
|
||||
* FileStreamer is utility class that will (optionally) recurse through all directories and
|
||||
* feed a Java Stream of useful directory walking detail (disk, directory, file, and the
|
||||
* textual path to get there).
|
||||
* <p>
|
||||
* Sample usage:
|
||||
* <pre>
|
||||
* FileStreamer.forDisk(image)
|
||||
* .ignoreErrors(true)
|
||||
* .stream()
|
||||
* .filter(this::fileFilter)
|
||||
* .forEach(fileHandler);
|
||||
* </pre>
|
||||
*
|
||||
* @author rob
|
||||
*/
|
||||
public class FileStreamer {
|
||||
private static final Consumer<FormattedDisk> NOOP_CONSUMER = d -> {};
|
||||
|
||||
public static FileStreamer forDisk(File file) throws IOException, DiskUnrecognizedException {
|
||||
return forDisk(file.getPath());
|
||||
}
|
||||
public static FileStreamer forDisk(String fileName) throws IOException, DiskUnrecognizedException {
|
||||
return new FileStreamer(new Disk(fileName));
|
||||
}
|
||||
public static FileStreamer forDisk(Disk disk) throws DiskUnrecognizedException {
|
||||
return new FileStreamer(disk);
|
||||
}
|
||||
|
||||
private FormattedDisk[] formattedDisks = null;
|
||||
|
||||
// Processor flags (used in gathering)
|
||||
private boolean ignoreErrorsFlag = false;
|
||||
private boolean recursiveFlag = true;
|
||||
|
||||
// Processor events
|
||||
private Consumer<FormattedDisk> beforeDisk = NOOP_CONSUMER;
|
||||
private Consumer<FormattedDisk> afterDisk = NOOP_CONSUMER;
|
||||
|
||||
// Filters
|
||||
private Predicate<FileTuple> filters = this::deletedFileFilter;
|
||||
private boolean includeDeletedFlag = false;
|
||||
private List<PathMatcher> pathMatchers = new ArrayList<>();
|
||||
|
||||
private FileStreamer(Disk disk) throws DiskUnrecognizedException {
|
||||
this.formattedDisks = disk.getFormattedDisks();
|
||||
}
|
||||
|
||||
public FileStreamer ignoreErrors(boolean flag) {
|
||||
this.ignoreErrorsFlag = flag;
|
||||
return this;
|
||||
}
|
||||
public FileStreamer recursive(boolean flag) {
|
||||
this.recursiveFlag = flag;
|
||||
return this;
|
||||
}
|
||||
public FileStreamer matchGlobs(List<String> globs) {
|
||||
if (globs != null && !globs.isEmpty()) {
|
||||
FileSystem fs = FileSystems.getDefault();
|
||||
for (String glob : globs) {
|
||||
pathMatchers.add(fs.getPathMatcher("glob:" + glob));
|
||||
}
|
||||
this.filters = filters.and(this::globFilter);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public FileStreamer matchGlobs(String... globs) {
|
||||
return matchGlobs(Arrays.asList(globs));
|
||||
}
|
||||
public FileStreamer includeTypeOfFile(TypeOfFile type) {
|
||||
this.filters = filters.and(type.predicate);
|
||||
return this;
|
||||
}
|
||||
public FileStreamer includeDeleted(boolean flag) {
|
||||
this.includeDeletedFlag = flag;
|
||||
return this;
|
||||
}
|
||||
public FileStreamer beforeDisk(Consumer<FormattedDisk> consumer) {
|
||||
this.beforeDisk = consumer;
|
||||
return this;
|
||||
}
|
||||
public FileStreamer afterDisk(Consumer<FormattedDisk> consumer) {
|
||||
this.afterDisk = consumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Stream<FileTuple> stream() {
|
||||
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(), 0), false)
|
||||
.filter(filters);
|
||||
}
|
||||
public Iterator<FileTuple> iterator() {
|
||||
return new FileTupleIterator();
|
||||
}
|
||||
|
||||
protected boolean deletedFileFilter(FileTuple tuple) {
|
||||
return includeDeletedFlag || !tuple.fileEntry.isDeleted();
|
||||
}
|
||||
protected boolean globFilter(FileTuple tuple) {
|
||||
if (tuple.fileEntry.isDirectory()) {
|
||||
// If we don't match directories, no files can be listed.
|
||||
return true;
|
||||
}
|
||||
// This may cause issues, but Path is a "real" filesystem construct, so the delimiters
|
||||
// vary by OS (likely just "/" and "\"). However, Java also erases them to some degree,
|
||||
// so using "/" (as used in ProDOS) will likely work out.
|
||||
// Also note that we check the single file "PARMS.S" and full path "SOURCE/PARMS.S" since
|
||||
// the user might have entered "*.S" or something like "SOURCE/PARMS.S".
|
||||
FileSystem fs = FileSystems.getDefault();
|
||||
Path filePath = Paths.get(tuple.fileEntry.getFilename());
|
||||
Path fullPath = Paths.get(String.join(fs.getSeparator(), tuple.paths),
|
||||
tuple.fileEntry.getFilename());
|
||||
for (PathMatcher pathMatcher : pathMatchers) {
|
||||
if (pathMatcher.matches(filePath) || pathMatcher.matches(fullPath)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class FileTupleIterator implements Iterator<FileTuple> {
|
||||
private LinkedList<FileTuple> files = new LinkedList<>();
|
||||
private FormattedDisk currentDisk;
|
||||
|
||||
private FileTupleIterator() {
|
||||
for (FormattedDisk formattedDisk : formattedDisks) {
|
||||
files.addAll(toTupleList(FileTuple.of(formattedDisk)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
boolean hasNext = !files.isEmpty();
|
||||
if (hasNext) {
|
||||
FileTuple tuple = files.peek();
|
||||
// Was there a disk switch?
|
||||
if (tuple.formattedDisk != currentDisk) {
|
||||
if (currentDisk != null) {
|
||||
afterDisk.accept(currentDisk);
|
||||
}
|
||||
currentDisk = tuple.formattedDisk;
|
||||
beforeDisk.accept(currentDisk);
|
||||
}
|
||||
} else {
|
||||
if (currentDisk != null) {
|
||||
afterDisk.accept(currentDisk);
|
||||
}
|
||||
currentDisk = null;
|
||||
}
|
||||
return hasNext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileTuple next() {
|
||||
if (hasNext()) {
|
||||
FileTuple tuple = files.removeFirst();
|
||||
if (recursiveFlag && tuple.fileEntry.isDirectory()) {
|
||||
FileTuple newTuple = tuple.pushd(tuple.fileEntry);
|
||||
files.addAll(0, toTupleList(newTuple));
|
||||
}
|
||||
return tuple;
|
||||
} else {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
}
|
||||
|
||||
private List<FileTuple> toTupleList(FileTuple tuple) {
|
||||
List<FileTuple> list = new ArrayList<>();
|
||||
try {
|
||||
for (FileEntry fileEntry : tuple.directoryEntry.getFiles()) {
|
||||
list.add(tuple.of(fileEntry));
|
||||
}
|
||||
} catch (DiskException e) {
|
||||
if (!ignoreErrorsFlag) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package io.github.applecommander.filestreamer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DirectoryEntry;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
public class FileTuple {
|
||||
private static final Logger LOG = Logger.getLogger(FileTuple.class.getName());
|
||||
public final FormattedDisk formattedDisk;
|
||||
public final List<String> paths;
|
||||
public final DirectoryEntry directoryEntry;
|
||||
public final FileEntry fileEntry;
|
||||
|
||||
private FileTuple(FormattedDisk formattedDisk,
|
||||
List<String> paths,
|
||||
DirectoryEntry directoryEntry,
|
||||
FileEntry fileEntry) {
|
||||
this.formattedDisk = formattedDisk;
|
||||
this.paths = Collections.unmodifiableList(paths);
|
||||
this.directoryEntry = directoryEntry;
|
||||
this.fileEntry = fileEntry;
|
||||
}
|
||||
|
||||
public FileTuple pushd(FileEntry directoryEntry) {
|
||||
LOG.fine("Adding directory " + directoryEntry.getFilename());
|
||||
List<String> newPaths = new ArrayList<>(paths);
|
||||
newPaths.add(directoryEntry.getFilename());
|
||||
return new FileTuple(formattedDisk, newPaths, (DirectoryEntry)directoryEntry, null);
|
||||
}
|
||||
public FileTuple of(FileEntry fileEntry) {
|
||||
return new FileTuple(formattedDisk, paths, directoryEntry, fileEntry);
|
||||
}
|
||||
|
||||
public static FileTuple of(FormattedDisk disk) {
|
||||
return new FileTuple(disk, new ArrayList<String>(), (DirectoryEntry)disk, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package io.github.applecommander.filestreamer;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public enum TypeOfFile {
|
||||
FILE(tuple -> !tuple.fileEntry.isDirectory()),
|
||||
DIRECTORY(tuple -> tuple.fileEntry.isDirectory()),
|
||||
BOTH(tuple -> true);
|
||||
|
||||
public final Predicate<FileTuple> predicate;
|
||||
|
||||
private TypeOfFile(Predicate<FileTuple> predicate) {
|
||||
this.predicate = predicate;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package io.github.applecommander.filters;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FileFilter;
|
||||
import com.webcodepro.applecommander.storage.os.dos33.DosFileEntry;
|
||||
import com.webcodepro.applecommander.storage.os.prodos.ProdosFileEntry;
|
||||
|
||||
import io.github.applecommander.applesingle.AppleSingle;
|
||||
|
||||
/**
|
||||
* A FileFilter to write each file to an independent AppleSingle file.
|
||||
*/
|
||||
public class AppleSingleFileFilter implements FileFilter {
|
||||
@Override
|
||||
public byte[] filter(FileEntry fileEntry) {
|
||||
try {
|
||||
AppleSingle.Builder builder = AppleSingle.builder()
|
||||
.dataFork(fileEntry.getFileData())
|
||||
.realName(fileEntry.getFilename());
|
||||
if (fileEntry instanceof ProdosFileEntry) {
|
||||
handleProDOS(builder, (ProdosFileEntry)fileEntry);
|
||||
}
|
||||
else if (fileEntry instanceof DosFileEntry) {
|
||||
handleDOS(builder, (DosFileEntry)fileEntry);
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
builder.build().save(baos);
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
protected void handleProDOS(AppleSingle.Builder builder, ProdosFileEntry prodos) {
|
||||
// We can't get the "access" byte so reconstructing it...
|
||||
int access = (prodos.canDestroy() ? 0x80 : 0x00)
|
||||
| (prodos.canRename() ? 0x40 : 0x00)
|
||||
| (prodos.canWrite() ? 0x02 : 0x00)
|
||||
| (prodos.canRead() ? 0x01 : 0x00);
|
||||
builder.access(access);
|
||||
builder.auxType(prodos.getAuxiliaryType());
|
||||
builder.creationDate(prodos.getCreationDate().toInstant());
|
||||
builder.fileType(prodos.getFiletypeByte());
|
||||
builder.modificationDate(prodos.getLastModificationDate().toInstant());
|
||||
}
|
||||
protected void handleDOS(AppleSingle.Builder builder, DosFileEntry dos) {
|
||||
switch (dos.getFiletype()) {
|
||||
// 0x00 T
|
||||
case "T":
|
||||
builder.fileType(0x04); // TXT
|
||||
break;
|
||||
// 0x01 I
|
||||
case "I":
|
||||
builder.fileType(0xfa); // INT
|
||||
break;
|
||||
// 0x02 A
|
||||
case "A":
|
||||
builder.fileType(0xfc); // BAS
|
||||
break;
|
||||
// 0x04 B
|
||||
case "B":
|
||||
builder.fileType(0x06); // BIN
|
||||
//builder.auxType(???) // FIXME address is not exposed?
|
||||
break;
|
||||
// The rest we just default
|
||||
default:
|
||||
builder.fileType(0xf1); // $F1 (???)
|
||||
break;
|
||||
}
|
||||
builder.access(dos.isLocked() ? 0x01 : 0xc3);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSuggestedFileName(FileEntry fileEntry) {
|
||||
String fileName = fileEntry.getFilename().trim();
|
||||
if (!fileName.toLowerCase().endsWith(".as")) {
|
||||
fileName = fileName + ".as";
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package io.github.applecommander.filters;
|
||||
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FileFilter;
|
||||
|
||||
/**
|
||||
* A custom FileFilter to dump "raw" data from the disk.
|
||||
* This filter uses the filename as given on the Disk with
|
||||
* no additional extensions.
|
||||
*
|
||||
* @author rob
|
||||
*/
|
||||
public class RawFileFilter implements FileFilter {
|
||||
|
||||
@Override
|
||||
public byte[] filter(FileEntry fileEntry) {
|
||||
return fileEntry.getFileData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSuggestedFileName(FileEntry fileEntry) {
|
||||
return fileEntry.getFilename();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package io.github.applecommander.acx.converter;
|
||||
|
||||
import static io.github.applecommander.acx.converter.DataSizeConverter.KB;
|
||||
import static io.github.applecommander.acx.converter.DataSizeConverter.MB;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class DataSizeConverterTest {
|
||||
@Test
|
||||
public void testFormat() {
|
||||
assertEquals("1B", DataSizeConverter.format(1));
|
||||
assertEquals("100B", DataSizeConverter.format(100));
|
||||
assertEquals("2KB", DataSizeConverter.format(2*KB));
|
||||
assertEquals("140KB", DataSizeConverter.format(140*KB));
|
||||
assertEquals("800KB", DataSizeConverter.format(800*KB));
|
||||
assertEquals("5MB", DataSizeConverter.format(5*MB));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvert() throws Exception {
|
||||
DataSizeConverter converter = new DataSizeConverter();
|
||||
assertEquals(140*KB, (int)converter.convert("140kb"));
|
||||
assertEquals(800*KB, (int)converter.convert("800KB"));
|
||||
assertEquals(5*MB, (int)converter.convert("5Mb"));
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package io.github.applecommander.filestreamer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.webcodepro.applecommander.storage.DiskUnrecognizedException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class FileStreamerTest {
|
||||
private static final List<String> EXPECTED_MERLIN = Arrays.asList(
|
||||
"PRODOS", "MERLIN.SYSTEM", "PARMS", "ED", "ED.16",
|
||||
"SOURCEROR", "SOURCEROR/OBJ", "SOURCEROR/LABELS", "SOURCEROR/LABELS.S",
|
||||
"LIBRARY", "LIBRARY/SENDMSG.S", "LIBRARY/PRDEC.S", "LIBRARY/FPMACROS.S",
|
||||
"LIBRARY/MACROS.S", "LIBRARY/ROCKWELL.S",
|
||||
"SOURCE", "SOURCE/PARMS.S", "SOURCE/EDMAC.S", "SOURCE/KEYMAC.S",
|
||||
"SOURCE/PRINTFILER.S", "SOURCE/MAKE.DUMP.S", "SOURCE/CLOCK.S",
|
||||
"SOURCE/PI.START.S", "SOURCE/PI.MAIN.S", "SOURCE/PI.LOOK.S",
|
||||
"SOURCE/PI.DIV.S", "SOURCE/PI.ADD.S", "SOURCE/PI.MACS.S",
|
||||
"SOURCE/PI.NAMES.S",
|
||||
"UTILITIES", "UTILITIES/REMOVE.ED", "UTILITIES/EDMAC", "UTILITIES/CLOCK.12.ED",
|
||||
"UTILITIES/XREF", "UTILITIES/XREFA", "UTILITIES/FORMATTER",
|
||||
"UTILITIES/PRINTFILER", "UTILITIES/MON.65C02", "UTILITIES/MAKE.DUMP",
|
||||
"UTILITIES/CONV.REL.LNK", "UTILITIES/CONV.LNK.REL",
|
||||
"UTILITIES/CLR.HI.BIT", "UTILITIES/KEYMAC",
|
||||
"PI", "PI/NAMES", "PI/START", "PI/MAIN", "PI/LOOK", "PI/DIV", "PI/ADD", "PI/OBJ"
|
||||
);
|
||||
private static final List<String> EXPECTED_UNIDOS = Arrays.asList(
|
||||
"HELLO", "FORMATTER", "FORMATTER.OBJ", "MFID", "FUD", // Disk #1
|
||||
"HELLO", "MFID", "FUD" // Disk #2
|
||||
);
|
||||
|
||||
@Test
|
||||
public void testRecursiveListMerlin() throws DiskUnrecognizedException, IOException {
|
||||
List<String> actual =
|
||||
FileStreamer.forDisk("./src/test/resources/disks/MERLIN8PRO1.DSK")
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.map(this::makeFullPath)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(EXPECTED_MERLIN, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonRecursiveListMerlin() throws DiskUnrecognizedException, IOException {
|
||||
List<String> actual =
|
||||
FileStreamer.forDisk("./src/test/resources/disks/MERLIN8PRO1.DSK")
|
||||
.recursive(false)
|
||||
.stream()
|
||||
.map(this::makeFullPath)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> expected = EXPECTED_MERLIN.stream()
|
||||
.filter(s -> !s.contains("/"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListUnidos() throws DiskUnrecognizedException, IOException {
|
||||
List<String> actual =
|
||||
FileStreamer.forDisk("./src/test/resources/disks/UniDOS_3.3.dsk")
|
||||
.recursive(true)
|
||||
.stream()
|
||||
.map(this::makeFullPath)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(EXPECTED_UNIDOS, actual);
|
||||
}
|
||||
|
||||
|
||||
private String makeFullPath(FileTuple tuple) {
|
||||
if (tuple.paths == null || tuple.paths.isEmpty()) {
|
||||
return tuple.fileEntry.getFilename();
|
||||
} else {
|
||||
return String.join("/", String.join("/", tuple.paths), tuple.fileEntry.getFilename());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package io.github.applecommander.filestreamer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.webcodepro.applecommander.storage.Disk;
|
||||
import com.webcodepro.applecommander.storage.DiskException;
|
||||
import com.webcodepro.applecommander.storage.FileEntry;
|
||||
import com.webcodepro.applecommander.storage.FormattedDisk;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class FileTupleTest {
|
||||
@Test
|
||||
public void test() throws IOException, DiskException {
|
||||
Disk disk = new Disk("./src/test/resources/disks/MERLIN8PRO1.DSK");
|
||||
FormattedDisk formattedDisk = disk.getFormattedDisks()[0];
|
||||
FileTuple tuple = FileTuple.of(formattedDisk);
|
||||
FileEntry sourcerorDir = tuple.formattedDisk.getFile("SOURCEROR");
|
||||
tuple = tuple.pushd(sourcerorDir);
|
||||
FileEntry labelsSource = tuple.directoryEntry.getFiles().get(2);
|
||||
tuple = tuple.of(labelsSource);
|
||||
|
||||
assertEquals(Arrays.asList("SOURCEROR"), tuple.paths);
|
||||
assertEquals(formattedDisk, tuple.formattedDisk);
|
||||
assertEquals(sourcerorDir, tuple.directoryEntry);
|
||||
assertEquals(labelsSource, tuple.fileEntry);
|
||||
}
|
||||
}
|
BIN
app/ac-acx/src/test/resources/disks/MERLIN8PRO1.DSK
Normal file
BIN
app/ac-acx/src/test/resources/disks/MERLIN8PRO1.DSK
Normal file
Binary file not shown.
BIN
app/ac-acx/src/test/resources/disks/UniDOS_3.3.dsk
Normal file
BIN
app/ac-acx/src/test/resources/disks/UniDOS_3.3.dsk
Normal file
Binary file not shown.
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '2.6.1'
|
||||
id 'org.springframework.boot' version "$springBoot"
|
||||
id 'application'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -13,3 +13,5 @@ antVersion=1.8.2
|
||||
commonsLang3Version=3.7
|
||||
commonsCsvVersion=1.8
|
||||
gsonVersion=2.8.6
|
||||
picocliVersion=4.6.2
|
||||
springBoot=2.6.1
|
||||
|
@ -7,6 +7,9 @@ plugins {
|
||||
ext.isSnapshotVersion = version.endsWith("SNAPSHOT")
|
||||
ext.isReleaseVersion = !ext.isSnapshotVersion
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -2,6 +2,9 @@ plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
sourceCompatibility = 11
|
||||
targetCompatibility = 11
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ rootProject.name = "AppleCommander"
|
||||
|
||||
include ':lib:ac-api'
|
||||
include ':lib:ac-swt-common'
|
||||
include ':app:ac-acx'
|
||||
include ':app:ac-cli'
|
||||
include ':app:ac-swing'
|
||||
include ':app:ac-swt-macosx-aarch64'
|
||||
|
Loading…
Reference in New Issue
Block a user