Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4d9da140 authored by Mike Lockwood's avatar Mike Lockwood
Browse files

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: Id5365e1c4dc55a2d819c16c9db0a3ac2260f9309
Signed-off-by: default avatarMike Lockwood <lockwood@android.com>
parent 462accab
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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,
+6 −0
Original line number Diff line number Diff line
@@ -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      },
};

+225 −6
Original line number Diff line number Diff line
@@ -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
+22 −1
Original line number Diff line number Diff line
@@ -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
+13 −0
Original line number Diff line number Diff line
@@ -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