DO NOT MERGE MTP: Add extended operations to support in-place editing of files

MTP does not support partial writes of files (the entire file must be transferred at once).
This makes it impossible to implement a FUSE file system for MTP
with acceptable performance.
To fix this problem, this change adds extended MTP operations to allow
partial writes to files:

SendPartialObject - allows writing a subset of a file, or appending to the end of a file

TruncateObject - allows changing the size of a file

BeginEditObject - must be called before using SendPartialObject and TruncateObject

EndEditObject - commits changes to a file after it has been edited with SendPartialObject or TruncateObject

We also add GetPartialObject64, which is the same as GetPartialObject
but has a 64 bit offset rather than 32.

Change-Id: I000930b787b00a2da0b57de9790053b2d71b86fd
Signed-off-by: Mike Lockwood <lockwood@android.com>
This commit is contained in:
Mike Lockwood
2011-04-21 17:05:55 -07:00
parent fb6232635d
commit fdb50e6f8e
6 changed files with 280 additions and 38 deletions

View File

@ -29,6 +29,7 @@
#include "MtpDatabase.h"
#include "MtpDataPacket.h"
#include "MtpObjectInfo.h"
#include "MtpProperty.h"
#include "MtpStringBuffer.h"
#include "MtpUtils.h"
@ -138,7 +139,7 @@ public:
MtpDataPacket& packet);
virtual MtpResponseCode getObjectInfo(MtpObjectHandle handle,
MtpDataPacket& packet);
MtpObjectInfo& info);
virtual MtpResponseCode getObjectFilePath(MtpObjectHandle handle,
MtpString& outFilePath,
@ -746,7 +747,7 @@ MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle,
}
MtpResponseCode MyMtpDatabase::getObjectInfo(MtpObjectHandle handle,
MtpDataPacket& packet) {
MtpObjectInfo& info) {
char date[20];
JNIEnv* env = AndroidRuntime::getJNIEnv();
@ -756,46 +757,27 @@ MtpResponseCode MyMtpDatabase::getObjectInfo(MtpObjectHandle handle,
return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
jint* intValues = env->GetIntArrayElements(mIntBuffer, 0);
MtpStorageID storageID = intValues[0];
MtpObjectFormat format = intValues[1];
MtpObjectHandle parent = intValues[2];
info.mStorageID = intValues[0];
info.mFormat = intValues[1];
info.mParent = intValues[2];
env->ReleaseIntArrayElements(mIntBuffer, intValues, 0);
jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
uint64_t size = longValues[0];
uint64_t modified = longValues[1];
info.mCompressedSize = (size > 0xFFFFFFFFLL ? 0xFFFFFFFF : size);
info.mDateModified = longValues[1];
env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
// int associationType = (format == MTP_FORMAT_ASSOCIATION ?
// info.mAssociationType = (format == MTP_FORMAT_ASSOCIATION ?
// MTP_ASSOCIATION_TYPE_GENERIC_FOLDER :
// MTP_ASSOCIATION_TYPE_UNDEFINED);
int associationType = MTP_ASSOCIATION_TYPE_UNDEFINED;
packet.putUInt32(storageID);
packet.putUInt16(format);
packet.putUInt16(0); // protection status
packet.putUInt32((size > 0xFFFFFFFFLL ? 0xFFFFFFFF : size));
packet.putUInt16(0); // thumb format
packet.putUInt32(0); // thumb compressed size
packet.putUInt32(0); // thumb pix width
packet.putUInt32(0); // thumb pix height
packet.putUInt32(0); // image pix width
packet.putUInt32(0); // image pix height
packet.putUInt32(0); // image bit depth
packet.putUInt32(parent);
packet.putUInt16(associationType);
packet.putUInt32(0); // association desc
packet.putUInt32(0); // sequence number
info.mAssociationType = MTP_ASSOCIATION_TYPE_UNDEFINED;
jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
packet.putString(str); // file name
MtpString temp(str);
info.mName = strdup((const char *)temp);
env->ReleaseCharArrayElements(mStringBuffer, str, 0);
packet.putEmptyString();
formatDateTime(modified, date, sizeof(date));
packet.putString(date); // date modified
packet.putEmptyString(); // keywords
checkAndClearExceptionFromCallback(env, __FUNCTION__);
return MTP_RESPONSE_OK;
}

View File

@ -23,6 +23,7 @@ namespace android {
class MtpDataPacket;
class MtpProperty;
class MtpObjectInfo;
class MtpDatabase {
public:
@ -81,7 +82,7 @@ public:
MtpDataPacket& packet) = 0;
virtual MtpResponseCode getObjectInfo(MtpObjectHandle handle,
MtpDataPacket& packet) = 0;
MtpObjectInfo& info) = 0;
virtual MtpResponseCode getObjectFilePath(MtpObjectHandle handle,
MtpString& outFilePath,

View File

@ -63,6 +63,12 @@ static const CodeEntry sOperationCodes[] = {
{ "MTP_OPERATION_GET_OBJECT_REFERENCES", 0x9810 },
{ "MTP_OPERATION_SET_OBJECT_REFERENCES", 0x9811 },
{ "MTP_OPERATION_SKIP", 0x9820 },
// android extensions
{ "MTP_OPERATION_GET_PARTIAL_OBJECT_64", 0x95C1 },
{ "MTP_OPERATION_SEND_PARTIAL_OBJECT", 0x95C2 },
{ "MTP_OPERATION_TRUNCATE_OBJECT", 0x95C3 },
{ "MTP_OPERATION_BEGIN_EDIT_OBJECT", 0x95C4 },
{ "MTP_OPERATION_END_EDIT_OBJECT", 0x95C5 },
{ 0, 0 },
};

View File

@ -30,6 +30,7 @@
#include "MtpDebug.h"
#include "MtpDatabase.h"
#include "MtpObjectInfo.h"
#include "MtpProperty.h"
#include "MtpServer.h"
#include "MtpStorage.h"
@ -79,6 +80,12 @@ static const MtpOperationCode kSupportedOperationCodes[] = {
MTP_OPERATION_GET_OBJECT_REFERENCES,
MTP_OPERATION_SET_OBJECT_REFERENCES,
// MTP_OPERATION_SKIP,
// Android extension for direct file IO
MTP_OPERATION_GET_PARTIAL_OBJECT_64,
MTP_OPERATION_SEND_PARTIAL_OBJECT,
MTP_OPERATION_TRUNCATE_OBJECT,
MTP_OPERATION_BEGIN_EDIT_OBJECT,
MTP_OPERATION_END_EDIT_OBJECT,
};
static const MtpEventCode kSupportedEventCodes[] = {
@ -218,6 +225,15 @@ void MtpServer::run() {
}
}
// commit any open edits
int count = mObjectEditList.size();
for (int i = 0; i < count; i++) {
ObjectEdit* edit = mObjectEditList[i];
commitEdit(edit);
delete edit;
}
mObjectEditList.clear();
if (mSessionOpen)
mDatabase->sessionEnded();
}
@ -252,6 +268,44 @@ void MtpServer::sendEvent(MtpEventCode code, uint32_t param1) {
}
}
void MtpServer::addEditObject(MtpObjectHandle handle, MtpString& path,
uint64_t size, MtpObjectFormat format, int fd) {
ObjectEdit* edit = new ObjectEdit;
edit->handle = handle;
edit->path = path;
edit->size = size;
edit->format = format;
edit->fd = fd;
mObjectEditList.add(edit);
}
MtpServer::ObjectEdit* MtpServer::getEditObject(MtpObjectHandle handle) {
int count = mObjectEditList.size();
for (int i = 0; i < count; i++) {
ObjectEdit* edit = mObjectEditList[i];
if (edit->handle == handle) return edit;
}
return NULL;
}
void MtpServer::removeEditObject(MtpObjectHandle handle) {
int count = mObjectEditList.size();
for (int i = 0; i < count; i++) {
ObjectEdit* edit = mObjectEditList[i];
if (edit->handle == handle) {
delete edit;
mObjectEditList.removeAt(i);
return;
}
}
LOGE("ObjectEdit not found in removeEditObject");
}
void MtpServer::commitEdit(ObjectEdit* edit) {
mDatabase->endSendObject((const char *)edit->path, edit->handle, edit->format, true);
}
bool MtpServer::handleRequest() {
Mutex::Autolock autoLock(mMutex);
@ -322,7 +376,8 @@ bool MtpServer::handleRequest() {
response = doGetObject();
break;
case MTP_OPERATION_GET_PARTIAL_OBJECT:
response = doGetPartialObject();
case MTP_OPERATION_GET_PARTIAL_OBJECT_64:
response = doGetPartialObject(operation);
break;
case MTP_OPERATION_SEND_OBJECT_INFO:
response = doSendObjectInfo();
@ -339,6 +394,18 @@ bool MtpServer::handleRequest() {
case MTP_OPERATION_GET_DEVICE_PROP_DESC:
response = doGetDevicePropDesc();
break;
case MTP_OPERATION_SEND_PARTIAL_OBJECT:
response = doSendPartialObject();
break;
case MTP_OPERATION_TRUNCATE_OBJECT:
response = doTruncateObject();
break;
case MTP_OPERATION_BEGIN_EDIT_OBJECT:
response = doBeginEditObject();
break;
case MTP_OPERATION_END_EDIT_OBJECT:
response = doEndEditObject();
break;
default:
LOGE("got unsupported command %s", MtpDebug::getOperationCodeName(operation));
response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
@ -363,7 +430,7 @@ MtpResponseCode MtpServer::doGetDeviceInfo() {
mData.putUInt16(MTP_STANDARD_VERSION);
mData.putUInt32(6); // MTP Vendor Extension ID
mData.putUInt16(MTP_STANDARD_VERSION);
string.set("microsoft.com: 1.0;");
string.set("microsoft.com: 1.0; android.com: 1.0;");
mData.putString(string); // MTP Extensions
mData.putUInt16(0); //Functional Mode
mData.putAUInt16(kSupportedOperationCodes,
@ -601,7 +668,40 @@ MtpResponseCode MtpServer::doGetObjectInfo() {
if (!hasStorage())
return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
MtpObjectHandle handle = mRequest.getParameter(1);
return mDatabase->getObjectInfo(handle, mData);
MtpObjectInfo info(handle);
MtpResponseCode result = mDatabase->getObjectInfo(handle, info);
if (result == MTP_RESPONSE_OK) {
char date[20];
mData.putUInt32(info.mStorageID);
mData.putUInt16(info.mFormat);
mData.putUInt16(info.mProtectionStatus);
// if object is being edited the database size may be out of date
uint32_t size = info.mCompressedSize;
ObjectEdit* edit = getEditObject(handle);
if (edit)
size = (edit->size > 0xFFFFFFFFLL ? 0xFFFFFFFF : (uint32_t)edit->size);
mData.putUInt32(size);
mData.putUInt16(info.mThumbFormat);
mData.putUInt32(info.mThumbCompressedSize);
mData.putUInt32(info.mThumbPixWidth);
mData.putUInt32(info.mThumbPixHeight);
mData.putUInt32(info.mImagePixWidth);
mData.putUInt32(info.mImagePixHeight);
mData.putUInt32(info.mImagePixDepth);
mData.putUInt32(info.mParent);
mData.putUInt16(info.mAssociationType);
mData.putUInt32(info.mAssociationDesc);
mData.putUInt32(info.mSequenceNumber);
mData.putString(info.mName);
mData.putEmptyString(); // date created
formatDateTime(info.mDateModified, date, sizeof(date));
mData.putString(date); // date modified
mData.putEmptyString(); // keywords
}
return result;
}
MtpResponseCode MtpServer::doGetObject() {
@ -641,12 +741,22 @@ MtpResponseCode MtpServer::doGetObject() {
return MTP_RESPONSE_OK;
}
MtpResponseCode MtpServer::doGetPartialObject() {
MtpResponseCode MtpServer::doGetPartialObject(MtpOperationCode operation) {
if (!hasStorage())
return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
MtpObjectHandle handle = mRequest.getParameter(1);
uint32_t offset = mRequest.getParameter(2);
uint32_t length = mRequest.getParameter(3);
uint64_t offset;
uint32_t length;
offset = mRequest.getParameter(2);
if (operation == MTP_OPERATION_GET_PARTIAL_OBJECT_64) {
// android extension with 64 bit offset
uint64_t offset2 = mRequest.getParameter(3);
offset = offset | (offset2 << 32);
length = mRequest.getParameter(4);
} else {
// standard GetPartialObject
length = mRequest.getParameter(3);
}
MtpString pathBuf;
int64_t fileLength;
MtpObjectFormat format;
@ -933,4 +1043,113 @@ MtpResponseCode MtpServer::doGetDevicePropDesc() {
return MTP_RESPONSE_OK;
}
MtpResponseCode MtpServer::doSendPartialObject() {
if (!hasStorage())
return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
MtpObjectHandle handle = mRequest.getParameter(1);
uint64_t offset = mRequest.getParameter(2);
uint64_t offset2 = mRequest.getParameter(3);
offset = offset | (offset2 << 32);
uint32_t length = mRequest.getParameter(4);
ObjectEdit* edit = getEditObject(handle);
if (!edit) {
LOGE("object not open for edit in doSendPartialObject");
return MTP_RESPONSE_GENERAL_ERROR;
}
// can't start writing past the end of the file
if (offset > edit->size) {
LOGD("writing past end of object, offset: %lld, edit->size: %lld", offset, edit->size);
return MTP_RESPONSE_GENERAL_ERROR;
}
// read the header
int ret = mData.readDataHeader(mFD);
// FIXME - check for errors here.
// reset so we don't attempt to send this back
mData.reset();
const char* filePath = (const char *)edit->path;
LOGV("receiving partial %s %lld %ld\n", filePath, offset, length);
mtp_file_range mfr;
mfr.fd = edit->fd;
mfr.offset = offset;
mfr.length = length;
// transfer the file
ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr);
LOGV("MTP_RECEIVE_FILE returned %d", ret);
if (ret < 0) {
mResponse.setParameter(1, 0);
if (errno == ECANCELED)
return MTP_RESPONSE_TRANSACTION_CANCELLED;
else
return MTP_RESPONSE_GENERAL_ERROR;
}
mResponse.setParameter(1, length);
uint64_t end = offset + length;
if (end > edit->size) {
edit->size = end;
}
return MTP_RESPONSE_OK;
}
MtpResponseCode MtpServer::doTruncateObject() {
MtpObjectHandle handle = mRequest.getParameter(1);
ObjectEdit* edit = getEditObject(handle);
if (!edit) {
LOGE("object not open for edit in doTruncateObject");
return MTP_RESPONSE_GENERAL_ERROR;
}
uint64_t offset = mRequest.getParameter(2);
uint64_t offset2 = mRequest.getParameter(3);
offset |= (offset2 << 32);
if (ftruncate(edit->fd, offset) != 0) {
return MTP_RESPONSE_GENERAL_ERROR;
} else {
edit->size = offset;
return MTP_RESPONSE_OK;
}
}
MtpResponseCode MtpServer::doBeginEditObject() {
MtpObjectHandle handle = mRequest.getParameter(1);
if (getEditObject(handle)) {
LOGE("object already open for edit in doBeginEditObject");
return MTP_RESPONSE_GENERAL_ERROR;
}
MtpString path;
int64_t fileLength;
MtpObjectFormat format;
int result = mDatabase->getObjectFilePath(handle, path, fileLength, format);
if (result != MTP_RESPONSE_OK)
return result;
int fd = open((const char *)path, O_RDWR | O_EXCL);
if (fd < 0) {
LOGE("open failed for %s in doBeginEditObject (%d)", (const char *)path, errno);
return MTP_RESPONSE_GENERAL_ERROR;
}
addEditObject(handle, path, fileLength, format, fd);
return MTP_RESPONSE_OK;
}
MtpResponseCode MtpServer::doEndEditObject() {
MtpObjectHandle handle = mRequest.getParameter(1);
ObjectEdit* edit = getEditObject(handle);
if (!edit) {
LOGE("object not open for edit in doEndEditObject");
return MTP_RESPONSE_GENERAL_ERROR;
}
commitEdit(edit);
removeEditObject(handle);
return MTP_RESPONSE_OK;
}
} // namespace android

View File

@ -65,6 +65,17 @@ private:
Mutex mMutex;
// represents an MTP object that is being edited using the android extensions
// for direct editing (BeginEditObject, SendPartialObject, TruncateObject and EndEditObject)
struct ObjectEdit {
MtpObjectHandle handle;
MtpString path;
uint64_t size;
MtpObjectFormat format;
int fd;
};
Vector<ObjectEdit*> mObjectEditList;
public:
MtpServer(int fd, MtpDatabase* database,
int fileGroup, int filePerm, int directoryPerm);
@ -86,6 +97,12 @@ private:
void sendStoreRemoved(MtpStorageID id);
void sendEvent(MtpEventCode code, uint32_t param1);
void addEditObject(MtpObjectHandle handle, MtpString& path,
uint64_t size, MtpObjectFormat format, int fd);
ObjectEdit* getEditObject(MtpObjectHandle handle);
void removeEditObject(MtpObjectHandle handle);
void commitEdit(ObjectEdit* edit);
bool handleRequest();
MtpResponseCode doGetDeviceInfo();
@ -106,12 +123,16 @@ private:
MtpResponseCode doGetObjectPropList();
MtpResponseCode doGetObjectInfo();
MtpResponseCode doGetObject();
MtpResponseCode doGetPartialObject();
MtpResponseCode doGetPartialObject(MtpOperationCode operation);
MtpResponseCode doSendObjectInfo();
MtpResponseCode doSendObject();
MtpResponseCode doDeleteObject();
MtpResponseCode doGetObjectPropDesc();
MtpResponseCode doGetDevicePropDesc();
MtpResponseCode doSendPartialObject();
MtpResponseCode doTruncateObject();
MtpResponseCode doBeginEditObject();
MtpResponseCode doEndEditObject();
};
}; // namespace android

View File

@ -391,6 +391,19 @@
#define MTP_OPERATION_SET_OBJECT_REFERENCES 0x9811
#define MTP_OPERATION_SKIP 0x9820
// Android extensions for direct file IO
// Same as GetPartialObject, but with 64 bit offset
#define MTP_OPERATION_GET_PARTIAL_OBJECT_64 0x95C1
// Same as GetPartialObject64, but copying host to device
#define MTP_OPERATION_SEND_PARTIAL_OBJECT 0x95C2
// Truncates file to 64 bit length
#define MTP_OPERATION_TRUNCATE_OBJECT 0x95C3
// Must be called before using SendPartialObject and TruncateObject
#define MTP_OPERATION_BEGIN_EDIT_OBJECT 0x95C4
// Called to commit changes made by SendPartialObject and TruncateObject
#define MTP_OPERATION_END_EDIT_OBJECT 0x95C5
// MTP Response Codes
#define MTP_RESPONSE_UNDEFINED 0x2000
#define MTP_RESPONSE_OK 0x2001