[Karmalib-commits] java/src/dev/karma KarmaBackup.java,NONE,1.1
Brought to you by:
justinkwaugh
From: <chu...@us...> - 2004-02-18 02:19:48
|
Update of /cvsroot/karmalib/java/src/dev/karma In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv20122 Added Files: KarmaBackup.java Log Message: Initial version of KarmaBackup...a utility to mass back up your karma in a relatively intelligent fashion. --- NEW FILE: KarmaBackup.java --- /* * This file is licensed under the GNU Library or "Lesser" General * Public License (LGPL). * * See the file LGPL or LGPL.txt included in this release, or * http://opensource.org/licenses/lgpl-license.php * for the text of the license. * * Copyright (c) 2004 Scott C. Gray */ package dev.karma; import dev.karma.lib.AdvancedKarmaClient; import dev.karma.lib.KarmaTCPConnection; import dev.karma.lib.KarmaClientFactory; import dev.karma.lib.data.AccessRights; import dev.karma.lib.data.FileDetails; import dev.karma.lib.exception.KarmaException; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.util.Iterator; import java.util.Properties; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import java.util.regex.Matcher; /** * KarmaBackup is a utility (both command line or API-wise) for backing up * the contents of your Rio Karma. I, like a lot of people, didn't have * the luxury of ripping my 300-some-odd CD's on a single machine and no * one machine could store it all so my Karma is my only single repository * of many hours worth of work. This utility is designed to make as easy * as possible to maintain a live working image of the contents of your * Karma. * * <p>Usage of the KarmaBackup utility from the command line is * relatively straight forward: * * <pre> * java -cp /path/to/karmalib.jar dev.karmalib.KarmaBackup [options] ipaddress * </pre> * * <p>where <b>options</b> are: * * <ul> * <li> <b>--directory</b> <i>dir</i><br> * Specifies the root directory into which all files will be * extrated. File placed in this directory will be named * according to the template specified with the * <b>--template</b> option. The default directory is ".". * <li> <b>--password</b> <i>pass</i><br> * Specifies the password used to connect to the Karma * player. The default is "password". * <li> <b>--match</b> <i>regex</i><br> * If supplied, only files matching <i>regex</i> regular * expression will be extracted from the player. * <li> <b>--port</b> <i>port</i><br> * If supplied, overrides the default TCP/IP communications * port utilized to communicate with the player. * <li> <b>--blocksize</b> <i>bytes</i><br> * Specifies the number of bytes to transfer in each read * request to the karma. The default is 8192. * <li> <b>--template</b> <i>template</i><br> * Used to provide a templated description of how files * extracted from the player are to be named. This * template is a string that may contain the following * special macros: * * <ul> * <li> <b>%r</b> Will be replaced by the Artist * attribute of the track. * <li> <b>%a</b> Will be replaced by the Album * attribute of the track. * <li> <b>%t</b> Will be replaced by the Track Title. * <li> <b>%n</b> Will be replaced by the Track Number * attribute of the track. * </ul> * * Any path separators ("/" or "\") will be honored and * directories created as necessary. All other characters * are left intact. A "%%" may be used to create a literal * "%". The default template is: "%r/%a/%n - %t" (or * "Artist/Album/#TrackNum - TrackTitle"). * </ul> * * <p><i>ipaddress</i> Specifies the IP address of your player. * * @author <a href="mailto:sg...@in...">Scott C. Gray</a> * @version 1.0 */ public class KarmaBackup { /** * <b>%r</b> - File naming template variable for referring to the artist * of a song. */ public static final char ARTIST = 'r'; /** * <b>%a</b> - File naming template variable for referring to the album * that a song comes from. */ public static final char ALBUM = 'a'; /** * <b>%t</b> - File naming template variable for referring to the track * title of a song. */ public static final char TRACK_TITLE = 't'; /** * <b>%n</b> - File naming template variable for referring to the track * number of a song. */ public static final char TRACK_NUMBER = 'n'; /* * Handy to have around. */ private static final String SEP = System.getProperty("file.separator"); /** * The default file template: "%r/%a/%n - %t" (these will be * backslashes on Winderz). */ public static final String DEFAULT_FILE_TEMPLATE = "%" + ARTIST + SEP + "%" + ALBUM + SEP + "%" + TRACK_NUMBER + " - %" + TRACK_TITLE; /** * Default number of Bytes to process at a time. */ private static long DEFAULT_BLOCK_SIZE = 8192; private String fileTemplate = DEFAULT_FILE_TEMPLATE; private String hostname = null; private int port = KarmaTCPConnection.PORT; private String repository = null; private String password = null; private Pattern pattern = null; private long blockSize = DEFAULT_BLOCK_SIZE; /** * Creates an instance of a KarmaBackup. * * @param hostname The hostname (or, rather, IP address) of the Karma. * @param password The password required to connect to the Karma. * @param repository The base directory into which files are to be placed. * * @throws KarmaException Thrown if the instance cannot be created. * Most likely causes are failure to access the repository directory. */ public KarmaBackup (String hostname, String password, String repository) throws KarmaException { File dir; this.hostname = hostname; this.repository = repository; this.password = password; /* * Sanity checks... */ dir = new File(repository); if (dir.exists() == false) { throw new KarmaException("The repository directory '" + repository + "' does not exist."); } if (dir.isDirectory() == false) { throw new KarmaException("The supplied repository '" + repository + "' is not a directory."); } if (dir.canWrite() == false) { throw new KarmaException("The supplied repository '" + repository + "' is not writable."); } } /** * Creates an instance of a KarmaBackup. * * @param hostname The hostname (or, rather, IP address) of the Karma. * @param password The password required to connect to the Karma. * @param port The port of the Karma. * @param repository The base directory into which files are to be placed. * * @throws KarmaException Thrown if the instance cannot be created. * Most likely causes are failure to access the repository directory. */ public KarmaBackup (String hostname, String password, int port, String repository) throws KarmaException { this(hostname, password, repository); this.port = port; } /** * Sets the number of bytes to transfer in each read request to the karma. * * @param BlockSize number of bytes to transfer in each read request * to the karma. */ public void setBlockSize(long blockSize) { this.blockSize = blockSize; } /** * Gets the number of bytes to transfer in each read request to the karma. * * @return the number of bytes to transfer in each read request to the karma. */ public long getBlockSize() { return (this.blockSize); } /** * Creates a template utilized for naming the resulting file that * is extracted from the Karma. This template may utilize the * following special identifiers to refer to items in the ID2/3 * tag associated with the track. * * <ul> * <li> <b>%r</b> - Will be replaced with the Artist * <li> <b>%a</b> - Will be replaced with the Album * <li> <b>%t</b> - Will be replaced with the Track Title * <li> <b>%n</b> - Will be replaced with the Track Number * </ul> * * <p>All other characters within the template will be left in-tact. * A '%%' may be utilized to place a literal '%' in the template. * * @param fileTemplate The template to be utilized when placing * files. The default template is <b>DEFAULT_FILE_TEMPLATE</b>. */ public void setFileTemplate(String fileTemplate) { this.fileTemplate = fileTemplate; } /** * Retrieves the current file template. * * @return The current file template. */ public String getFileTemplate() { return (fileTemplate); } /** * Provides a regular expression that will be utilized to determine * which files get extracted from the Karma. This pattern is * compared to the expanded file template. * * @param pat A regular expression to use for comparisons. A * null pattern matches all files. */ public void setMatchPattern(String pat) throws PatternSyntaxException { if (pat == null) { this.pattern = null; } else { this.pattern = Pattern.compile(pat); } } /** * Gets A regular expression specifying which files are to be backed up * * @return A regular expression specifying which files are to be backed up */ public String getMatchPattern() { return (pattern == null ? null : pattern.toString()); } /** * Sets the hostname (or IP address) at which the Karma resides * * @param hostname the hostname (or IP address) at which the Karma resides */ public void setHostname(String hostname) { this.hostname = hostname; } /** * Gets the hostname (or IP address) at which the Karma resides * * @return the hostname (or IP address) at which the Karma resides */ public String getHostname() { return (this.hostname); } /** * Sets the port at which the Karma resides. * * @param port the port at which the Karma resides. */ public void setPort(int port) { this.port = port; } /** * Gets the port at which the Karma resides. * * @return the port at which the Karma resides. */ public int getPort() { return (this.port); } /** * Sets The directory in which all files will be placed * (based upon the expansion of the file template). * * @param repository The directory in which all files will be * placed. */ public void setRepository(String repository) { this.repository = repository; } /** * Gets The directory in which all files will be placed (based upon * the expansion of the file template). * * @return The directory in which all files will be placed. */ public String getRepository() { return (this.repository); } /** * Sets the connection password for communicating with the karma * * @param password the connection password for communicating with the karma */ public void setPassword(String password) { this.password = password; } /** * Gets the connection password for communicating with the karma * * @return the connection password for communicating with the karma */ public String getPassword() { return (this.password); } /** * Performs the actual backup opertion. This first connects to the * Karma, retrieves the directory listing, then gets to work. */ public void backup() throws KarmaException { KarmaTCPConnection conn = new KarmaTCPConnection(hostname, port); AdvancedKarmaClient karma = KarmaClientFactory.getAdvancedClient(conn); boolean locked = false; try { /* * Make sure we know how to talk to this thing. */ if (karma.isValidDeviceProtocolVersion() == false) { throw new KarmaException("Device located at '" + hostname + "' does " + "not report the appropriate protocol version. Aborting."); } /* * Now, lets authenticate. */ AccessRights rights = karma.login(password); if (rights.hasReadAccess() == false) { throw new KarmaException("The password you have supplied does not " + "have sufficient rights to extract contents from the device " + "located at '" + hostname + "'"); } System.out.println("Retrieving file details..."); karma.getWriteLock(); locked = true; FileDetails allDetails[] = karma.getAllFileDetails(); for (int i = 0; i < allDetails.length; i++) { FileDetails details = allDetails[i]; String name = expand(details); File file = new File(name); long length = details.getLength(); if (file.exists() && file.length() == length) { System.out.print("[Already Exists] "); System.out.println(name); } else if (pattern != null && pattern.matcher(name).find() == false) { System.out.print("[No Match] "); System.out.println(name); } else { if (file.exists()) { file.delete(); } System.out.print("[Extracting] "); System.out.println(name); copy(karma, details, file); } } } finally { if (locked) { try { karma.releaseIOLock(); } catch (KarmaException e) { System.err.println("Failed to release I/O lock: " + e.getMessage()); } } /* * Done. */ disconnect(karma); } } /** * Copies a file out of the karma. * * @param karma The handle to the player. * @param details Which file to extract. * @param toFile The name of the output file to create. * * @throws KarmaException If no worky. */ private void copy(AdvancedKarmaClient karma, FileDetails details, File toFile) throws KarmaException { createDirectory(toFile); try { OutputStream out = new BufferedOutputStream( new FileOutputStream(toFile)); int fid = details.getFileId(); long totalBytes = details.getLength(); long offset = 0; while (offset < totalBytes) { long nBytes = (totalBytes - offset); if (nBytes > blockSize) { nBytes = blockSize; } // System.out.println(" readFileChunk(" // + offset + ", " + nBytes + ", " + fid + ")"); karma.readFileChunk(offset, nBytes, fid, out); offset += nBytes; } out.close(); } catch (IOException e) { throw new KarmaException("Failed to write to `" + toFile.toString() + "': " + e.getMessage()); } } /** * Recursively make sure that a given directory structure * exists. * * @param file Path to a file. * * @throws IOException If the directory structure cannot be * created. */ private void createDirectory(File file) throws KarmaException { File directory = new File(file.getParent()); if (directory.isDirectory() == false) { if (directory.mkdirs() == false) { throw new KarmaException("Unable to create directory: " + directory.toString()); } } } /** * Given the details of a file, expands to a full path name * (including the repository base directory) utilizing the * configured file template. * * @param details The details of the file. * * @return An expanded path to store the file. */ private String expand(FileDetails details) { StringBuffer sb = new StringBuffer(); int idx = 0; int len = fileTemplate.length(); sb.append(repository); if (!repository.endsWith("/") && !repository.endsWith("\\")) { sb.append(SEP); } while (idx < len) { char ch = fileTemplate.charAt(idx); ++idx; if (ch == '%' && idx < len) { ch = fileTemplate.charAt(idx); ++idx; switch (ch) { case ARTIST: append(sb, details.getArtist()); break; case TRACK_TITLE: append(sb, details.getTitle()); break; case TRACK_NUMBER: String num = details.getTrackNumber(); if (num.length() < 2) { sb.append('0'); } append(sb, num); break; case ALBUM: append(sb, details.getSource()); break; default: sb.append('%'); sb.append(ch); } } else { sb.append(ch); } } /* * Last little bit. Automagically append the file extension. */ String codec = details.getCodec(); if ("vorbis".equals(codec)) { sb.append(".ogg"); } else { sb.append('.'); sb.append(codec); } return (sb.toString()); } /** * Used to append a string to a string buffer, protecting or * removing characters that would screw up a filename. * * @param sb Buffer * @param str String to append. */ private void append(StringBuffer sb, String str) { int len = str.length(); for (int i = 0; i < len; i++) { char ch = str.charAt(i); /* * There has to be a better way....I don't know which characters * are valid for the O/S I happen to be running on. */ if (Character.isLetterOrDigit(ch) || Character.isWhitespace(ch) || ch == '_' || ch == '(' || ch == ')' || ch == '!' || ch == '.' || ch == '+' || ch == '\'' || ch == '#' || ch == ',') { sb.append(ch); } } } /** * Helper to cleanly disconnect. * * @param karma The Karma handle to disconnect. */ private void disconnect(AdvancedKarmaClient karma) { try { karma.disconnect(); } catch (Exception e) { System.err.println("Error while disconnecting from Karma device at '" + hostname + "': " + e.getMessage()); } } /** * Runnable version of KarmaBackup. * * <pre> * usage: KarmaBackup [--password pwd] [--port port] [--directory dir] * [--template tmp] ipaddress * </pre> * * @param argv Command line arguments. */ public static void main (String argv[]) { String hostname; String directory = "."; String template = DEFAULT_FILE_TEMPLATE; String password = "password"; String match = null; int port = KarmaTCPConnection.PORT; long blockSize = DEFAULT_BLOCK_SIZE; KarmaBackup backup; int idx = 0; while (idx < argv.length && argv[idx].startsWith("--")) { if (argv[idx].equals("--password") && idx < (argv.length - 2)) { password = argv[idx + 1]; idx += 2; } else if (argv[idx].equals("--match") && idx < (argv.length - 2)) { match = argv[idx + 1]; idx += 2; } else if (argv[idx].equals("--port") && idx < (argv.length - 2)) { try { port = Integer.parseInt(argv[idx + 1]); } catch (Exception e) { System.err.println("--port: Invalid port number supplied: " + argv[idx + 1]); System.exit(1); } idx += 2; } else if (argv[idx].equals("--blocksize") && idx < (argv.length - 2)) { try { blockSize = Long.parseLong(argv[idx + 1]); } catch (Exception e) { System.err.println("--blocksize: Invalid value supplied: " + argv[idx + 1]); System.exit(1); } idx += 2; } else if (argv[idx].equals("--directory") && idx < (argv.length - 2)) { directory = argv[idx + 1]; idx += 2; } else if (argv[idx].equals("--template") && idx < (argv.length - 2)) { template = argv[idx + 1]; idx += 2; } else { printUsage(); System.exit(1); } } if (idx == argv.length) { printUsage(); System.exit(1); } hostname = argv[idx]; try { backup = new KarmaBackup(hostname, password, port, directory); backup.setFileTemplate(template); backup.setMatchPattern(match); backup.setBlockSize(blockSize); backup.backup(); } catch (PatternSyntaxException e) { System.err.println("--match: Invalid regular expression"); System.exit(1); } catch (KarmaException e) { System.err.println(e.getMessage()); System.exit(1); } catch (Exception e) { e.printStackTrace(System.err); System.exit(1); } System.exit(0); } /** * Prints main() usage to stdout. */ private static void printUsage() { System.out.println( "usage: KarmaBackup [options] ipaddress"); System.out.println( " --directory dir Directory to extract files (default '.')"); System.out.println( " --password pass Password required to connect (default 'password')"); System.out.println( " --match regex Regular expression that restricts what gets extracted"); System.out.println( " --port port Port to connect to (default '" + KarmaTCPConnection.PORT + "')"); System.out.println( " --template tmpl File nameing template. This may contain: "); System.out.println( " --blocksize bytes Number of bytes to read at a time (default " + DEFAULT_BLOCK_SIZE + ")"); System.out.println( " %r - Will be replaced with the Artist"); System.out.println( " %a - Will be replaced with the Album"); System.out.println( " %t - Will be replaced with the Track Title"); System.out.println( " %n - Will be replaced with the Track Number"); } } |