/*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Library General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program (see the file COPYING.LIB for more
* details); if not, write to the Free Software Foundation, Inc.,
* 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.dcache.chimera.cli;
import com.google.common.base.Optional;
import com.google.common.primitives.Booleans;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import diskCacheV111.util.AccessLatency;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.RetentionPolicy;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.Option;
import org.dcache.acl.ACE;
import org.dcache.acl.ACLException;
import org.dcache.acl.enums.RsType;
import org.dcache.acl.parser.ACEParser;
import org.dcache.chimera.ChimeraFsException;
import org.dcache.chimera.DirectoryStreamB;
import org.dcache.chimera.FileNotFoundHimeraFsException;
import org.dcache.chimera.FileSystemProvider;
import org.dcache.chimera.FsFactory;
import org.dcache.chimera.FsInode;
import org.dcache.chimera.HimeraDirectoryEntry;
import org.dcache.chimera.NotDirChimeraException;
import org.dcache.chimera.UnixPermission;
import org.dcache.chimera.namespace.ChimeraOsmStorageInfoExtractor;
import org.dcache.chimera.namespace.ChimeraStorageInfoExtractable;
import org.dcache.chimera.namespace.ExtendedInode;
import org.dcache.chimera.posix.Stat;
import org.dcache.util.Args;
import org.dcache.util.Checksum;
import org.dcache.util.ChecksumType;
import org.dcache.util.cli.ShellApplication;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.padStart;
import static com.google.common.io.ByteStreams.toByteArray;
import static java.util.stream.Collectors.toList;
import static org.dcache.chimera.FileSystemProvider.StatCacheOption.STAT;
import static org.dcache.util.ByteUnit.KiB;
public class Shell extends ShellApplication
{
private final FileSystemProvider fs;
private final ChimeraStorageInfoExtractable extractor;
private String path = "/";
private FsInode pwd;
public static void main(String[] arguments) throws Throwable {
if (arguments.length < 6) {
System.err.println("Usage: chimera <jdbcUrl> <dbUser> <dbPass> " +
"<storageInfoExtractor> <accessLatency> <retentionPolicy>");
System.exit(4);
}
Args args = new Args(arguments);
args.shift(6);
try (Shell shell = new Shell(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4], arguments[5])) {
shell.start(args);
}
}
public Shell(String url, String user, String password, String extractor, String accessLatency, String retentionPolicy) throws Exception
{
fs = FsFactory.createFileSystem(url, user, password);
pwd = fs.path2inode(path);
Class<? extends ChimeraOsmStorageInfoExtractor> storageInfoExtractor =
Class.forName(extractor).asSubclass(ChimeraOsmStorageInfoExtractor.class);
Constructor<? extends ChimeraStorageInfoExtractable> constructor = storageInfoExtractor.getConstructor(AccessLatency.class, RetentionPolicy.class);
this.extractor = (ChimeraStorageInfoExtractable) constructor.newInstance(
AccessLatency.getAccessLatency(accessLatency),
RetentionPolicy.getRetentionPolicy(retentionPolicy));
}
@Override
protected String getCommandName()
{
return "chimera";
}
@Override
protected String getPrompt()
{
return "chimera:" + path + "# ";
}
@Override
public void close() throws IOException
{
fs.close();
}
@Nonnull
private FsInode lookup(@Nullable File path) throws ChimeraFsException
{
if (path == null) {
return pwd;
} else if (path.isAbsolute()) {
return fs.path2inode(path.toString());
} else {
return fs.path2inode(path.toString(), pwd);
}
}
@Command(name = "cd", hint = "change current directory")
public class CdCommand implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws ChimeraFsException
{
pwd = lookup(path);
Shell.this.path = (pwd.getParent() != null) ? fs.inode2path(pwd) : "/";
return null;
}
}
@Command(name = "chgrp", hint = "change file group",
description = "The chgrp command sets the group ID of PATH to GID. Mapped group names " +
"cannot be used.")
public class ChgrpCommand implements Callable<Serializable>
{
@Argument(index = 0)
int gid;
@Argument(index = 1)
File path;
@Override
public Serializable call() throws ChimeraFsException
{
Stat stat = new Stat();
stat.setGid(gid);
lookup(path).setStat(stat);
return null;
}
}
@Command(name = "chmod", hint = "change file mode",
description = "The chmod command modifies the file mode bits of PATH to MODE. The MODE must " +
"be expressed as an octal bit mask.")
public class ChmodCommand implements Callable<Serializable>
{
@Argument(index = 0)
String mode; // octal
@Argument(index = 1)
File path;
@Override
public Serializable call() throws ChimeraFsException
{
Stat stat = new Stat();
stat.setMode(Integer.parseInt(mode, 8));
lookup(path).setStat(stat);
return null;
}
}
@Command(name = "chown", hint = "change file owner and group",
description = "The chown command sets the owner of PATH to UID. Mapped user names " +
"cannot be used.")
public class ChownCommand implements Callable<Serializable>
{
@Argument(index = 0, valueSpec = "UID[:GID]")
String owner;
@Argument(index = 1)
File path;
private int _uid;
private int _gid;
private void parseOwner(String ownership)
{
int colon = ownership.indexOf(':');
if (colon == -1) {
_uid = parseInteger(ownership);
_gid = -1;
} else {
checkArgument(colon > 0 && colon < ownership.length() - 1,
"Colon must separate two integers.");
_uid = parseInteger(ownership.substring(0, colon));
_gid = parseInteger(ownership.substring(colon + 1));
checkArgument(_gid >= 0, "GID must be 0 or greater.");
}
checkArgument(_uid >= 0, "UID must be 0 or greater.");
}
private int parseInteger(String value)
{
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Only integer values are allowed and \"" + value +"\" is not an integer.");
}
}
@Override
public Serializable call() throws ChimeraFsException
{
parseOwner(owner);
Stat stat = new Stat();
stat.setUid(_uid);
if (_gid != -1) {
stat.setGid(_gid);
}
FsInode inode = lookup(path);
inode.setStat(stat);
return null;
}
}
@Command(name = "ls", hint = "list directory contents")
public class LsCommand implements Callable<Serializable>
{
private static final String DEFAULT_TIME = "mtime";
/* The block size is purely nominal; we use 1KiB here as historically
* filesystems have used a 1KiB block size. */
private final int BLOCK_SIZE = KiB.toBytes(1);
private final int[] INT_SIZE_TABLE = {9, 99, 999, 9999, 99999,
999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE};
private final DateFormat WITH_YEAR =
new SimpleDateFormat("MMM dd yyyy");
private final DateFormat WITHOUT_YEAR =
new SimpleDateFormat("MMM dd HH:mm");
private int nlinkWidth = 0;
private int uidWidth = 0;
private int gidWidth = 0;
private int sizeWidth = 0;
private final long sixMonthsInPast = sixMonthsInPast();
private final long oneHourInFuture = oneHourInFuture();
@Option(name = "time", values = { "access", "use", "atime", "status", "ctime", "modify", "mtime", "create" },
usage = "Show alternative time instead of modification time: access/use/atime is the last access time, " +
"status/ctime is the last file status modification time, modify/mtime is the last write time, " +
"create is the creation time.")
String time = DEFAULT_TIME;
@Option(name = "c",
usage = "Use time of last modification of the file status information instead of last modification " +
"of the file itself.")
boolean ctime;
@Option(name = "u",
usage = "Use time of last access instead of last modification of the file.")
boolean atime;
@Argument(required = false)
File path;
@Override
public Serializable call() throws IOException
{
checkArgument(Booleans.countTrue(atime, ctime, time != DEFAULT_TIME) <= 1,
"Conflicting time arguments.");
if (ctime) {
time = "ctime";
} else if (atime) {
time = "atime";
}
List<HimeraDirectoryEntry> entries = new LinkedList<>();
long totalBlocks = 0;
HimeraDirectoryEntry dot = null;
HimeraDirectoryEntry dotdot = null;
FsInode inode = lookup(path);
try (DirectoryStreamB<HimeraDirectoryEntry> dirStream = inode.newDirectoryStream()) {
for (HimeraDirectoryEntry entry : dirStream) {
String name = entry.getName();
Stat stat = entry.getStat();
switch (name) {
case ".":
dot = entry;
break;
case "..":
dotdot = entry;
break;
default:
entries.add(entry);
break;
}
totalBlocks = updateTotalBlocks(totalBlocks, stat);
nlinkWidth = updateMaxWidth(nlinkWidth, stat.getNlink());
uidWidth = updateMaxWidth(uidWidth, stat.getUid());
gidWidth = updateMaxWidth(gidWidth, stat.getGid());
sizeWidth = updateMaxWidth(sizeWidth, stat.getSize());
}
}
console.println("total " + totalBlocks);
printEntry(dot);
printEntry(dotdot);
for (HimeraDirectoryEntry entry : entries) {
printEntry(entry);
}
return null;
}
private void printEntry(HimeraDirectoryEntry entry) throws IOException
{
if (entry != null) {
Stat stat = entry.getStat();
long time;
switch (this.time) {
case "access":
case "atime":
case "use":
time = stat.getATime();
break;
case "status":
case "ctime":
time = stat.getCTime();
break;
case "modify":
case "mtime":
time = stat.getMTime();
break;
case "create":
time = stat.getCrTime();
break;
default:
throw new IllegalArgumentException("Unknown time field: " + this.time);
}
String s = String.format("%s %s %s %s %s %s %s",
permissionsFor(stat),
pad(stat.getNlink(), nlinkWidth),
pad(stat.getUid(), uidWidth),
pad(stat.getGid(), gidWidth),
pad(stat.getSize(), sizeWidth),
dateOf(time),
entry.getName());
console.println(s);
}
}
// For files with a time that is more than 6 months old or more than 1
// hour into the future, the timestamp contains the year instead of the
// time of day.
private String dateOf(long time)
{
Date d = new Date(time);
if(time < sixMonthsInPast || time > oneHourInFuture) {
return WITH_YEAR.format(d);
} else {
return WITHOUT_YEAR.format(d);
}
}
private long sixMonthsInPast()
{
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MONTH, -6);
return calendar.getTimeInMillis();
}
private long oneHourInFuture()
{
return System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);
}
private String pad(long value, int width)
{
String str = String.valueOf(value);
return padStart(str, width, ' ');
}
private long updateTotalBlocks(long total, Stat stat)
{
// calculate number of blocks, but rounding up
long nBlocks = 1 + (stat.getSize() -1)/ BLOCK_SIZE;
return total + nBlocks;
}
private int updateMaxWidth(int max, int value)
{
int width = widthOf(value);
return width > max ? width : max;
}
private int updateMaxWidth(int max, long value)
{
int width = widthOf(value);
return width > max ? width : max;
}
private String permissionsFor(Stat stat)
{
return new UnixPermission(stat.getMode()).toString();
}
// Requires positive x
private int widthOf(int x)
{
for (int i=0; ; i++) {
if (x <= INT_SIZE_TABLE[i]) {
return i+1;
}
}
}
// Requires positive x
private int widthOf(long x)
{
if(x <= Integer.MAX_VALUE) {
return widthOf((int) x);
}
// x is more than 0x7fffffff or 2147483647
if (x < 1000000000000L) { // from 10 to 12 digits
if (x < 10000000000L) {
return 10;
} else {
return x < 100000000000L ? 11 : 12;
}
} else { // 13 or more digits
if (x < 10000000000000000L) {
if (x < 100000000000000L) {
return x < 10000000000000L ? 13 : 14;
} else {
return x < 1000000000000000L ? 15 : 16;
}
} else {
if (x < 1000000000000000000L) {
return x < 100000000000000000L ? 17 : 18;
} else {
return 19;
}
}
}
}
}
@Command(name = "lstag", hint = "list directory tags")
public class LstagCommand implements Callable<Serializable>
{
@Argument(required = false)
File path;
@Override
public Serializable call() throws IOException
{
String[] tags = fs.tags(lookup(path));
console.println("Total: " + tags.length);
for (String tag : tags) {
console.println(tag);
}
return null;
}
}
@Command(name = "mkdir", hint = "make directory")
public class MkdirCommand implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws ChimeraFsException
{
if (path.isAbsolute()) {
fs.mkdir(path.toString());
} else {
fs.mkdir(Shell.this.path + '/' + path);
}
return null;
}
}
@Command(name = "mv", hint = "move file",
description = "Renames or moves SOURCE to TARGET. If TARGET is a directory, the source " +
"file is moved into TARGET.")
public class MvCommand implements Callable<Serializable>
{
@Argument(index = 0, metaVar = "source",
usage = "File to move or rename.")
File source;
@Argument(index = 1, metaVar = "target",
usage = "Target path or directory.")
File destination;
@Override
public Serializable call() throws ChimeraFsException
{
FsInode dst;
try {
dst = lookup(destination);
} catch (FileNotFoundHimeraFsException e) {
dst = null;
}
FsInode srcDir = lookup(source.getParentFile());
FsInode inode = srcDir.inodeOf(source.getName(), STAT);
if (dst != null && dst.isDirectory()) {
fs.rename(inode, srcDir, source.getName(), lookup(destination), source.getName());
} else {
fs.rename(inode, srcDir, source.getName(), lookup(destination.getParentFile()),
destination.getName());
}
return null;
}
}
@Command(name = "rm", hint = "remove a file",
description = "The rm command deletes the file. If the file has data " +
"stored in dCache then dCache will remove that data in a " +
"timely fashion.")
public class RmCommand implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws ChimeraFsException
{
if (lookup(path).isDirectory()) {
throw new ChimeraFsException(path + " is a directory");
}
if (path.isAbsolute()) {
fs.remove(path.toString());
} else {
fs.remove(Shell.this.path + '/' + path);
}
return null;
}
}
@Command(name = "rmdir", hint = "remove directory")
public class RmdirCommand implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws ChimeraFsException
{
FsInode inode = lookup(path);
if (!inode.isDirectory()) {
throw new NotDirChimeraException(inode);
}
fs.remove(inode);
return null;
}
}
@Command(name = "readtag", hint = "display tag data")
public class ReadTagCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1)
String tag;
@Override
public Serializable call() throws IOException, CacheException
{
FsInode inode = lookup(path);
Stat stat = fs.statTag(inode, tag);
byte[] data = new byte[(int) stat.getSize()];
fs.getTag(inode, tag, data, 0, data.length);
console.println(new String(data));
return null;
}
}
@Command(name = "get AccessLatency", hint = "display access latency of files/directories")
public class GetAccessLatencyCommand implements Callable<Serializable>
{
@Argument(required = false)
File path;
@Override
public Serializable call() throws IOException, CacheException
{
ExtendedInode inode = new ExtendedInode(fs, lookup(path));
AccessLatency accessLatency = extractor.getAccessLatency(inode);
console.println(accessLatency.toString());
return null;
}
}
@Command(name = "get RetentionPolicy", hint = "display retention policy of files/directories")
public class GetRetentionPolicyCommand implements Callable<Serializable>
{
@Argument(required = false)
File path;
@Override
public Serializable call() throws IOException, CacheException
{
ExtendedInode inode = new ExtendedInode(fs, lookup(path));
RetentionPolicy retentionPolicy = extractor.getRetentionPolicy(inode);
console.println(retentionPolicy.toString());
return null;
}
}
@Command(name = "writetag", hint = "write tag data")
public class WriteTagCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1)
String tag;
@Argument(index = 2, required = false)
String data;
@Override
public Serializable call() throws IOException
{
FsInode inode = lookup(path);
try {
fs.statTag(inode, tag);
} catch (FileNotFoundHimeraFsException fnf) {
fs.createTag(inode, tag);
}
byte[] bytes = (data == null)
? toByteArray(console.getInput())
: newLineTerminated(data).getBytes();
if (bytes.length > 0) {
fs.setTag(inode, tag, bytes, 0, bytes.length);
}
return null;
}
private String newLineTerminated(String s)
{
return s.endsWith("\n") ? s : s + '\n';
}
}
@Command(name = "rmtag", hint = "remove tag from directory")
public class RmTagCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1)
String tag;
@Override
public Serializable call() throws ChimeraFsException
{
FsInode inode = lookup(path);
fs.removeTag(inode, tag);
return null;
}
}
@Command(name = "setfacl", hint = "change access control lists",
description = "Sets a new ACL consisting of one or more ACEs to a resource (a file or directory), " +
"which is defined by its pnfsId or globalPath.\n\n" +
"Each ACE defines permissions to access this resource " +
"for a subject (a user or group of users). " +
"ACEs are ordered by significance, i.e., first match wins.\n\n" +
"Description of the ACE structure. \n\n" +
"The element <subject> defines the subject of the ACE and " +
"must be one of the following values: \n" +
" USER:<who_id> : user identified by the virtual user ID <who_id> \n" +
" GROUP:<who_id> : group identified by the virtual group ID <who_id> \n" +
" OWNER@ : user who owns the resource\n" +
" GROUP@ : group that owns the resource \n" +
" EVERYONE@ : world, including the owner and owning group\n" +
" ANONYMOUS@ : accessed without any authentication \n" +
" AUTHENTICATED@ : any authenticated user (opposite of ANONYMOUS) \n\n" +
"The MASK is a set of bits describing how correspondent permissions will " +
"be modified for users matching the SUBJECT. If MASK is preceded by a '+' then " +
"corresponding operations are allowed. If it is preceded by a '-' then " +
"corresponding operations are disallowed. Some bits apply only to regular " +
"files, others apply only to directories, and some to both. A bit is converted " +
"to the appropriate one, as indicated in parentheses.\n\n" +
"The following access permissions may be used: \n" +
" r : Permission to read the data of a file (converted to 'l' if directory). \n" +
" l : Permission to list the contents of a directory (converted to 'r' if file). \n" +
" w : Permission to modify a file's data anywhere in the file's offset range.\n" +
" This includes the ability to write to any arbitrary offset and \n" +
" as a result to grow the file. (Converted to 'f' if directory).\n" +
" f : Permission to add a new file in a directory (converted to 'w' if file).\n" +
" a : The ability to modify a file's data, but only starting at EOF \n"+
" (converted to 's' if directory).\n" +
" s : Permission to create a subdirectory in a directory (converted to 'a' if file).\n" +
" x : Permission to execute a file or traverse a directory.\n" +
" d : Permission to delete the file or directory.\n" +
" D : Permission to delete a file or directory within a directory.\n" +
" n : Permission to read the named attributes of a file or to lookup \n" +
" the named attributes directory.\n" +
" N : Permission to write the named attributes of a file or \n" +
" to create a named attribute directory.\n" +
" t : The ability to read basic attributes (non-ACLs) of a file or directory.\n" +
" T : Permission to change the times associated with a file \n" +
" or directory to an arbitrary value.\n" +
" c : Permission to read the ACL.\n" +
" C : Permission to write the acl and mode attributes.\n" +
" o : Permission to write the owner and owner group attributes.\n\n" +
"To enable ACL inheritance, the optional element <flags> must be defined. " +
"Multiple bits may be specified as a simple concatenated list of letters. " +
"Order doesn't matter.\n" +
" f : Can be placed on a directory and indicates that this ACE \n" +
" should be added to each new file created.\n" +
" d : Can be placed on a directory and indicates that this ACE \n" +
" should be added to each new directory created.\n" +
" o : Can be placed on a directory and indicates that this ACE \n" +
" should be ignored for this directory.\n" +
" Any ACE that inherit from an ACE with 'o' flag set will not have the 'o' flag set.\n" +
" Therefore, ACEs with this bit set take effect only if they are inherited \n" +
" by newly created files or directories as specified by the above two flags.\n" +
" REMARK: If 'o' flag is present on an ACE, then \n" +
" either 'd' or 'f' (or both) must be present as well.\n\n" +
"Examples: \n" +
"setfacl /pnfs/example.org/data/TestDir USER:3750:+lfs:d EVERYONE@:+l GROUP:8745:-s USER:3452:+D\n" +
" Permissions for TestDir are altered so: \n" +
" First ACE: User with id 3750 (USER:3750) is allowed to \n"+
" list directory contents (l), \n" +
" create files (f), \n"+
" create subdirectories (s), \n" +
" and these permissions will be inherited by all newly created \n" +
" subdirectories as well (d). \n" +
" Second ACE: Everyone (EVERYONE@) is allowed to \n"+
" list directory contents. \n" +
" Third ACE: Group with id 8745 (GROUP:8745) is not allowed to \n" +
" create subdirectories.\n" +
" Fourth ACE: User with id 3452 (USER:3452) is allowed to \n" +
" delete objects within this directory (D). The user must also have \n" +
" the delete permission (d) for the object to be deleted. See next example.\n\n " +
" \n" +
"setfacl /pnfs/example.org/data/TestDir/TestFile USER:3452:+d\n" +
" Permissions for TestFile are altered so: \n" +
" User with id 3452 (USER:3452) is allowed to \n" +
" delete this resource (d). To delete TestFile, the user must also \n"+
" have permission to delete directory contents (D). See previous example.\n\n" +
"For further information on ACLs in dCache please refer to: " +
"http://trac.dcache.org/trac.cgi/wiki/Integrate")
public class SetFaclCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1, valueSpec = "SUBJECT:+|-MASK[:FLAGS]")
String[] acl;
@Override
public Serializable call() throws ChimeraFsException
{
fs.setACL(lookup(path), Stream.of(acl).map(ACEParser::parse).collect(toList()));
return null;
}
}
@Command(name = "getfacl", hint = "display access control lists")
public class GetFaclComamnd implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws IOException, ACLException
{
FsInode inode = lookup(path);
List<ACE> acl = fs.getACL(inode);
for (ACE ace : acl) {
console.println(ace.toExtraFormat(inode.isDirectory() ? RsType.DIR : RsType.FILE));
}
return null;
}
}
@Command(name = "writedata", hint = "write file content",
description = "Be aware that such data is stored in the Chimera database and not in dCache. " +
"The data will not be accessible through dCache.")
public class WriteDataCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1, required = false)
String data;
@Override
public Serializable call() throws IOException
{
byte[] bytes = data == null
? toByteArray(System.in)
: newLineTerminated(data).getBytes();
writeDataIntoFile(bytes);
return null;
}
private void writeDataIntoFile(byte[] data)
throws ChimeraFsException
{
FsInode inode;
try {
inode = lookup(path);
} catch (FileNotFoundHimeraFsException fnf) {
if (path.isAbsolute()) {
inode = fs.createFile(path.toString());
} else {
inode = fs.createFile(lookup(path.getParentFile()), path.getName());
}
}
fs.setInodeIo(inode, true);
inode.write(0, data, 0, data.length);
}
private String newLineTerminated(String s)
{
return s.endsWith("\n") ? s : s + '\n';
}
}
@Command(name = "checksum list", hint = "list checksums of file")
public class ChecksumListCommand implements Callable<Serializable>
{
@Argument
File path;
@Override
public Serializable call() throws IOException
{
FsInode inode = lookup(path);
for (Checksum checksum : fs.getInodeChecksums(inode)) {
console.println(checksum.getType().getName() + ':' + checksum.getValue());
}
return null;
}
}
@Command(name = "checksum get", hint = "display checksum of file")
public class ChecksumGetComamnd implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1, valueSpec = "adler32|md5_type|md4_type")
ChecksumType type;
@Override
public Serializable call() throws IOException
{
FsInode inode = lookup(path);
Optional<Checksum> checksum = Checksum.forType(fs.getInodeChecksums(inode), type);
if (checksum.isPresent()) {
console.println(checksum.get().getValue());
} else {
console.println("No checksum of type " + type.getName());
}
return null;
}
}
@Command(name = "checksum add", hint = "add checksum to file")
public class ChecksumAddCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1, valueSpec = "adler32|md5_type|md4_type")
ChecksumType type;
@Argument(index = 2)
String checksum;
@Override
public Serializable call() throws ChimeraFsException
{
Checksum c = new Checksum(type, checksum);
FsInode inode = lookup(path);
if (inode.isDirectory() || inode.isLink()) {
throw new ChimeraFsException("Not a regular file: " + path);
}
fs.setInodeChecksum(inode, type.getType(), c.getValue());
return null;
}
}
@Command(name = "checksum delete", hint = "remove checkusm from file")
public class ChecksumDeleteCommand implements Callable<Serializable>
{
@Argument(index = 0)
File path;
@Argument(index = 1, valueSpec = "adler32|md5_type|md4_type")
ChecksumType type;
@Override
public Serializable call() throws ChimeraFsException
{
FsInode inode = lookup(path);
fs.removeInodeChecksum(inode, type.getType());
return null;
}
}
}