org.robovm.libimobiledevice.AfcClient Maven / Gradle / Ivy
/*
* Copyright (C) 2013 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.robovm.libimobiledevice;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
import org.robovm.libimobiledevice.binding.AfcClientRef;
import org.robovm.libimobiledevice.binding.AfcClientRefOut;
import org.robovm.libimobiledevice.binding.AfcError;
import org.robovm.libimobiledevice.binding.AfcFileMode;
import org.robovm.libimobiledevice.binding.AfcLinkType;
import org.robovm.libimobiledevice.binding.IntOut;
import org.robovm.libimobiledevice.binding.LibIMobileDevice;
import org.robovm.libimobiledevice.binding.LibIMobileDeviceConstants;
import org.robovm.libimobiledevice.binding.LockdowndServiceDescriptorStruct;
import org.robovm.libimobiledevice.binding.LongOut;
import org.robovm.libimobiledevice.binding.StringArray;
import org.robovm.libimobiledevice.binding.StringArrayOut;
/**
* Provides access to the device filesystem.
*/
public class AfcClient implements AutoCloseable {
public static final String SERVICE_NAME = LibIMobileDeviceConstants.AFC_SERVICE_NAME;
public static final String DEVICE_INFO_KEY_FS_TOTAL_BYTES = "FSTotalBytes";
public static final String DEVICE_INFO_KEY_FS_FREE_BYTES = "FSFreeBytes";
public static final String DEVICE_INFO_KEY_FS_BLOCK_SIZE = "FSBlockSize";
public static final String DEVICE_INFO_KEY_MODEL = "Model";
/**
* Creation time in nanos.
*/
public static final String FILE_INFO_KEY_ST_BIRTHTIME = "st_birthtime";
/**
* Last modification time in nanos.
*/
public static final String FILE_INFO_KEY_ST_MTIME = "st_mtime";
/**
* Number of blocks allocated for a file.
*/
public static final String FILE_INFO_KEY_ST_BLOCKS = "st_blocks";
/**
* Number of hard links.
*/
public static final String FILE_INFO_KEY_ST_NLINK = "st_nlink";
/**
* File size in bytes.
*/
public static final String FILE_INFO_KEY_ST_SIZE = "st_size";
/**
* File type. {@code S_IFREG} for regular files. {@code S_IFDIR} for
* directories. {@code S_IFLNK} for links.
*/
public static final String FILE_INFO_KEY_ST_IFMT = "st_ifmt";
/**
* Link target.
*/
public static final String FILE_INFO_KEY_LINK_TARGET = "LinkTarget";
protected AfcClientRef ref;
AfcClient(AfcClientRef ref) {
this.ref = ref;
}
/**
* Creates a new {@link AfcClient} and makes a connection to the
* {@code com.apple.afc} service on the device.
*
* @param device the device to connect to.
* @param service the service descriptor returned by {@link LockdowndClient#startService(String)}.
*/
public AfcClient(IDevice device, LockdowndServiceDescriptor service) {
if (device == null) {
throw new NullPointerException("device");
}
if (service == null) {
throw new NullPointerException("service");
}
AfcClientRefOut refOut = new AfcClientRefOut();
LockdowndServiceDescriptorStruct serviceStruct = new LockdowndServiceDescriptorStruct();
serviceStruct.setPort((short) service.getPort());
serviceStruct.setSslEnabled(service.isSslEnabled());
try {
checkResult(LibIMobileDevice.afc_client_new(device.getRef(), serviceStruct, refOut));
this.ref = refOut.getValue();
} finally {
serviceStruct.delete();
refOut.delete();
}
}
/**
* Returns a directory listing of the specified directory.
*
* @param dir the directory to list. Must be a fully-qualified path.
* @return the list of files in the specified directory.
*/
public String[] readDirectory(String dir) {
if (dir == null) {
throw new NullPointerException("dir");
}
StringArrayOut listOut = new StringArrayOut();
try {
checkResult(LibIMobileDevice.afc_read_directory(getRef(), dir, listOut));
StringArray list = listOut.getValue();
ArrayList result = new ArrayList();
if (list != null) {
for (int i = 0;; i++) {
String s = list.get(i);
if (s == null) {
break;
}
result.add(s);
}
}
return result.toArray(new String[result.size()]);
} finally {
LibIMobileDevice.delete_StringArray_values_z(listOut.getValue());
listOut.delete();
}
}
/**
* Retrieves device information. The information returned is the device
* model as well as the free space, the total capacity and blocksize on the
* accessed disk partition.
*
* @return the device info as key-value pairs. Possible keys are:
* {@link #DEVICE_INFO_KEY_MODEL}, {@link #DEVICE_INFO_KEY_FS_FREE_BYTES},
* {@link #DEVICE_INFO_KEY_FS_TOTAL_BYTES},
* {@link #DEVICE_INFO_KEY_FS_BLOCK_SIZE}.
*/
public Map getDeviceInfo() {
StringArrayOut infosOut = new StringArrayOut();
try {
checkResult(LibIMobileDevice.afc_get_device_info(getRef(), infosOut));
StringArray list = infosOut.getValue();
Map result = new TreeMap();
if (list != null) {
int i = 0;
while (true) {
String key = list.get(i++);
if (key == null) {
break;
}
String value = list.get(i++);
if (value == null) {
break;
}
result.put(key, value);
}
}
return result;
} finally {
LibIMobileDevice.delete_StringArray_values_z(infosOut.getValue());
infosOut.delete();
}
}
/**
* Returns the disk partition blocksize.
*
* @return the blocksize.
* @see #getDeviceInfo()
*/
public int getBlockSize() {
return Integer.parseInt(getDeviceInfo().get(DEVICE_INFO_KEY_FS_BLOCK_SIZE));
}
/**
* Returns the free space on the device in bytes.
*
* @return the free space in bytes.
* @see #getDeviceInfo()
*/
public long getFreeBytes() {
return Long.parseLong(getDeviceInfo().get(DEVICE_INFO_KEY_FS_FREE_BYTES));
}
/**
* Returns the total size of the device in bytes.
*
* @return the total size in bytes.
* @see #getDeviceInfo()
*/
public long getTotalBytes() {
return Long.parseLong(getDeviceInfo().get(DEVICE_INFO_KEY_FS_TOTAL_BYTES));
}
/**
* Returns the name of the device model.
*
* @return the device model name.
* @see #getDeviceInfo()
*/
public String getModel() {
return getDeviceInfo().get(DEVICE_INFO_KEY_MODEL);
}
/**
* Retrieves information for a specific file or directory.
*
* @param path the path of the file or directory.
* @return the file information as key-value pairs. Possible keys are:
* {@link #FILE_INFO_KEY_ST_BIRTHTIME}, {@link #FILE_INFO_KEY_ST_BLOCKS},
* {@link #FILE_INFO_KEY_ST_IFMT}, {@link #FILE_INFO_KEY_ST_MTIME},
* {@link #FILE_INFO_KEY_ST_NLINK}, {@link #FILE_INFO_KEY_ST_SIZE}.
*/
public Map getFileInfo(String path) {
StringArrayOut infolistOut = new StringArrayOut();
try {
checkResult(LibIMobileDevice.afc_get_file_info(getRef(), path, infolistOut));
StringArray list = infolistOut.getValue();
Map result = new TreeMap();
if (list != null) {
int i = 0;
while (true) {
String key = list.get(i++);
if (key == null) {
break;
}
String value = list.get(i++);
if (value == null) {
break;
}
result.put(key, value);
}
}
return result;
} finally {
LibIMobileDevice.delete_StringArray_values_z(infolistOut.getValue());
infolistOut.delete();
}
}
/**
* Opens a file on the device.
*
* @param path the fully-qualified path of the file to open.
* @param mode the mode to use to open the file.
* @return the handle to the open file.
*/
public long fileOpen(String path, AfcFileMode mode) {
LongOut handleOut = new LongOut();
try {
checkResult(LibIMobileDevice.afc_file_open(getRef(), path, mode, handleOut));
return handleOut.getValue();
} finally {
handleOut.delete();
}
}
/**
* Closes a file on the device.
*
* @param handle file handle of a previously opened file.
*/
public void fileClose(long handle) {
checkResult(LibIMobileDevice.afc_file_close(getRef(), handle));
}
/**
* Attempts to the read the given number of bytes from the given file.
*
* @param handle file handle of a previously opened file
* @param buffer the byte array in which to store the bytes read.
* @param offset the initial position in {@code buffer} to store the bytes
* read from the file.
* @param count the maximum number of bytes to store in {@code buffer}.
* @return the number of bytes actually read or -1 if the end of the stream
* has been reached.
*/
public int fileRead(long handle, byte[] buffer, int offset, int count) {
if ((offset | count) < 0 || offset > buffer.length || buffer.length - offset < count) {
throw new ArrayIndexOutOfBoundsException("length=" + buffer.length
+ "; regionStart=" + offset + "; regionLength=" + count);
}
if (count == 0) {
return 0;
}
byte[] data = buffer;
if (offset > 0) {
data = new byte[count];
}
IntOut bytesReadOut = new IntOut();
try {
checkResult(LibIMobileDevice.afc_file_read(getRef(), handle, data, count, bytesReadOut));
int bytesRead = bytesReadOut.getValue();
if (bytesRead == 0) {
// Assume EOF reached.
return -1;
}
if (data != buffer) {
System.arraycopy(data, 0, buffer, offset, bytesRead);
}
return bytesRead;
} finally {
bytesReadOut.delete();
}
}
/**
* Writes a given number of bytes to a file.
*
* @param handle file handle of previously opened file.
* @param buffer the buffer to be written.
* @param offset the start position in {@code buffer} from where to get bytes.
* @param count the number of bytes from {@code buffer} to write to the file.
* @return the number of bytes actually written to the file.
*/
public int fileWrite(long handle, byte[] buffer, int offset, int count) {
if ((offset | count) < 0 || offset > buffer.length || buffer.length - offset < count) {
throw new ArrayIndexOutOfBoundsException("length=" + buffer.length
+ "; regionStart=" + offset + "; regionLength=" + count);
}
if (count == 0) {
return 0;
}
byte[] data = buffer;
if (offset > 0) {
data = new byte[count];
System.arraycopy(buffer, offset, data, 0, count);
}
IntOut bytesWrittenOut = new IntOut();
try {
checkResult(LibIMobileDevice.afc_file_write(getRef(), handle, data, count, bytesWrittenOut));
int bytesWritten = bytesWrittenOut.getValue();
return bytesWritten;
} finally {
bytesWrittenOut.delete();
}
}
/**
* Copies the specified local {@link File} to the specified remote file.
*
* @param localFile the {@link File} to copy.
* @param remoteFile the path to the remote file.
*/
public void fileCopy(File localFile, String remoteFile) throws IOException {
long handle = fileOpen(remoteFile, AfcFileMode.AFC_FOPEN_WRONLY);
try (InputStream is = new FileInputStream(localFile)) {
int n = 0;
byte[] buffer = new byte[64 * 1024];
while ((n = is.read(buffer)) != -1) {
fileWrite(handle, buffer, 0, n);
}
} finally {
fileClose(handle);
}
}
/**
* Deletes a file or an empty directory.
*
* @param path the fully-qualified path to delete.
*/
public void removePath(String path) {
removePath(path, false);
}
/**
* Deletes a file or a directory hierarchy.
*
* @param path the fully-qualified path to delete.
* @param recurse if true
non-empty directories will be
* deleted recursively.
*/
public void removePath(String path, boolean recurse) {
if (!recurse) {
checkResult(LibIMobileDevice.afc_remove_path(getRef(), path));
} else {
AfcError rc = LibIMobileDevice.afc_remove_path(getRef(), path);
if (rc == AfcError.AFC_E_DIR_NOT_EMPTY) {
for (String child : readDirectory(path)) {
if (".".equals(child) || "..".equals(child)) {
continue;
}
removePath(stripDirSep(path) + "/" + child, true);
}
rc = LibIMobileDevice.afc_remove_path(getRef(), path);
}
checkResult(rc);
}
}
/**
* Same as removePath(recursive = true) but provided by lib itself
* @param path the fully-qualified path to delete.
*/
public void removePathAndContent(String path) {
AfcError result = LibIMobileDevice.afc_remove_path_and_contents(getRef(), path);
if (result != AfcError.AFC_E_SUCCESS && result != AfcError.AFC_E_OBJECT_NOT_FOUND) {
throw new LibIMobileDeviceException(result.swigValue(), result.name());
}
}
/**
* Renames a file or directory on the device.
*
* @param from the fully-qualified path of the file or directory to rename.
* @param to the fully-qualified path the file or directory should be
* renamed to.
*/
public void renamePath(String from, String to) {
checkResult(LibIMobileDevice.afc_rename_path(getRef(), from, to));
}
/**
* Creates a directory on the device. Does nothing if the directory already
* exists. Also creates parent directories recursively.
*
* @param dir the fully-qualified path of the directory to create.
*/
public void makeDirectory(String dir) {
checkResult(LibIMobileDevice.afc_make_directory(getRef(), dir));
}
/**
* Creates a hard link or symbolic link on the device.
*
* @param type the type of link to create.
* @param target the absolute or relative path of the link target.
* @param source the fully-qualified path where the link will be created.
*/
public void makeLink(AfcLinkType type, String target, String source) {
checkResult(LibIMobileDevice.afc_make_link(getRef(), type, target, source));
}
private String stripDirSep(String s) {
int end = s.length();
while (end > 0 && s.charAt(end - 1) == '/') {
end--;
}
return s.substring(0, end);
}
private String toAbsoluteDevicePath(String root, Path path) {
String child = toRelativeDevicePath(path);
return stripDirSep(root) + (child.length() > 0 ? "/" + toRelativeDevicePath(path) : "");
}
private String toRelativeDevicePath(Path path) {
StringBuilder sb = new StringBuilder();
int count = path.getNameCount();
for (int i = 0; i < count; i++) {
if (i > 0) {
sb.append('/');
}
sb.append(path.getName(i));
}
return sb.toString();
}
/**
* Uploads a local file or directory to the device.
*
* @param localFile the file or directory to upload.
* @param targetPath the path of the directory on the device where to place
* the uploaded files.
*/
public void upload(File localFile, final String targetPath) throws IOException {
upload(localFile, targetPath, null);
}
/**
* Uploads a local file or directory to the device.
*
* @param localFile the file or directory to upload.
* @param targetPath the path of the directory on the device where to place
* the uploaded files.
* @param callback callback which will receive progress and status updates.
* If null
no progress will be reported.
*/
public void upload(File localFile, final String targetPath,
final UploadProgressCallback callback) throws IOException {
final Path root = localFile.toPath().getParent();
// remove recursively destination folder as it might contain previous data
// due failed attempt
removePathAndContent(toAbsoluteDevicePath(targetPath, root.relativize(localFile.toPath())));
makeDirectory(targetPath);
// 64k seems to be a good buffer size. If smaller we will not get
// acceptable write speeds.
final byte[] buffer = new byte[64 * 1024];
class FileCounterVisitor extends SimpleFileVisitor {
int count;
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
count++;
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
count++;
return FileVisitResult.CONTINUE;
}
}
FileCounterVisitor visitor = new FileCounterVisitor();
if (callback != null) {
Files.walkFileTree(localFile.toPath(), visitor);
}
try {
final int fileCount = visitor.count;
Files.walkFileTree(localFile.toPath(), new SimpleFileVisitor() {
int filesUploaded = 0;
private void reportProgress(Path path) {
if (callback != null) {
callback.progress(path.toFile(), 100 * filesUploaded / fileCount);
}
filesUploaded++;
}
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) throws IOException {
reportProgress(dir);
String deviceDir = toAbsoluteDevicePath(targetPath, root.relativize(dir));
makeDirectory(deviceDir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
reportProgress(file);
String deviceFile = toAbsoluteDevicePath(targetPath, root.relativize(file));
if (Files.isSymbolicLink(file)) {
Path linkTargetPath = Files.readSymbolicLink(file);
makeLink(AfcLinkType.AFC_SYMLINK, toRelativeDevicePath(linkTargetPath), deviceFile);
} else if (Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
long fd = fileOpen(deviceFile, AfcFileMode.AFC_FOPEN_WRONLY);
try (InputStream is = Files.newInputStream(file)) {
int n = 0;
while ((n = is.read(buffer)) != -1) {
fileWrite(fd, buffer, 0, n);
}
} finally {
fileClose(fd);
}
}
return FileVisitResult.CONTINUE;
}
});
if (callback != null) {
callback.success();
}
} catch (IOException e) {
if (callback != null) {
callback.error(e.getMessage());
}
throw e;
} catch (LibIMobileDeviceException e) {
if (callback != null) {
callback.error(e.getMessage());
}
throw e;
}
}
protected AfcClientRef getRef() {
checkDisposed();
return ref;
}
protected final void checkDisposed() {
if (ref == null) {
throw new LibIMobileDeviceException("Already disposed");
}
}
public synchronized void dispose() {
checkDisposed();
LibIMobileDevice.afc_client_free(ref);
ref = null;
}
@Override
public void close() {
dispose();
}
private static void checkResult(AfcError result) {
if (result != AfcError.AFC_E_SUCCESS) {
throw new LibIMobileDeviceException(result.swigValue(), result.name());
}
}
public interface UploadProgressCallback {
/**
* Reports the progress of an upload to a device.
*
* @param path the path currently being uploaded.
* @param percentComplete the progress in percent.
*/
void progress(File path, int percentComplete);
/**
* Called once the upload has been completed successfully.
*/
void success();
/**
* Called if the upload fails.
*
* @param message the error message.
*/
void error(String message);
}
private void list(String path, boolean recurse) {
list(path, stripDirSep(path).replaceAll(".*?([^/]+)$", "$1"), "", recurse, new PrintWriter(System.out));
}
private void list(String path, String filename, String indent, boolean recurse, PrintWriter out) {
Map info = getFileInfo(path);
DateFormat df = new SimpleDateFormat("yyyyMMdd-HHmmss");
long birthTime = Long.parseLong(info.get(FILE_INFO_KEY_ST_BIRTHTIME)) / 1000 / 1000;
long mtime = Long.parseLong(info.get(FILE_INFO_KEY_ST_MTIME)) / 1000 / 1000;
long size = Long.parseLong(info.get(AfcClient.FILE_INFO_KEY_ST_SIZE));
if ("S_IFDIR".equals(info.get(AfcClient.FILE_INFO_KEY_ST_IFMT))) {
out.format("%s %s %9d (%s)\t%s%s/\n",
df.format(new Date(birthTime)),
df.format(new Date(mtime)),
size,
info.get(AfcClient.FILE_INFO_KEY_ST_IFMT),
indent, filename);
out.flush();
for (String f : readDirectory(path)) {
if (f.equals("..") || f.equals(".")) {
continue;
}
if (recurse) {
String childPath = path + "/" + f;
list(childPath, f, indent + " ", recurse, out);
}
}
} else if ("S_IFLNK".equals(info.get(AfcClient.FILE_INFO_KEY_ST_IFMT))) {
out.format("%s %s %9d (%s)\t%s%s -> %s\n",
df.format(new Date(birthTime)),
df.format(new Date(mtime)),
size,
info.get(AfcClient.FILE_INFO_KEY_ST_IFMT),
indent, filename, info.get(AfcClient.FILE_INFO_KEY_LINK_TARGET));
out.flush();
} else {
out.format("%s %s %9d (%s)\t%s%s\n",
df.format(new Date(birthTime)),
df.format(new Date(mtime)),
size,
info.get(AfcClient.FILE_INFO_KEY_ST_IFMT),
indent, filename);
out.flush();
}
}
private static void printUsageAndExit() {
System.err.println(AfcClient.class.getName() + " [deviceid] ...");
System.err.println(" Actions:");
System.err.println(" deviceinfo Prints device file system information.");
System.err.println(" rm [-f] Deletes from the device. Deletes non-empty dirs if -f is specified.");
System.err.println(" ls [-r] Lists the contents of the specified dir.");
System.err.println(" mkdir Creates the on the device.");
System.err.println(" mv Moves (renames) the remote path to .");
System.err.println(" upload \n"
+ " Uploads the local file or dir at to the remote dir .");
System.exit(0);
}
public static void main(String[] args) throws Exception {
String deviceId = null;
String action = null;
int index = 0;
try {
action = args[index++];
if (action.matches("[0-9a-f]{40}")) {
deviceId = action;
action = args[index++];
}
if (!action.matches("deviceinfo|rm|ls|mkdir|mv|upload")) {
System.err.println("Unknown action: " + action);
printUsageAndExit();
}
if (deviceId == null) {
String[] udids = IDevice.listUdids();
if (udids.length == 0) {
System.err.println("No device connected");
return;
}
if (udids.length > 1) {
System.err.println("More than 1 device connected ("
+ Arrays.asList(udids) + "). Using " + udids[0]);
}
deviceId = udids[0];
}
try (IDevice device = new IDevice(deviceId)) {
try (LockdowndClient lockdowndClient = new LockdowndClient(device, AfcClient.class.getSimpleName(), true)) {
LockdowndServiceDescriptor service = lockdowndClient.startService(SERVICE_NAME);
try (AfcClient client = new AfcClient(device, service)) {
boolean recurse = false;
switch (action) {
case "deviceinfo":
System.out.println(client.getDeviceInfo());
break;
case "rm":
if ("-r".equals(args[index])) {
recurse = true;
index++;
}
client.removePath(args[index], recurse);
break;
case "ls":
if ("-r".equals(args[index])) {
recurse = true;
index++;
}
client.list(args[index], recurse);
break;
case "mkdir":
client.makeDirectory(args[index]);
break;
case "mv":
client.renamePath(args[index++], args[index]);
break;
case "upload":
client.upload(new File(args[index++]), args[index], new UploadProgressCallback() {
public void progress(File path, int percentComplete) {
System.out.format("[%3d%%] Uploading %s\n", percentComplete, path);
}
public void success() {
System.out.format("[100%%] Upload done!\n");
}
public void error(String message) {
System.out.format("Error: %s\n", message);
}
});
break;
}
}
}
}
} catch (ArrayIndexOutOfBoundsException e) {
printUsageAndExit();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy