About QML and the new Yandex.Disk REST API

    Good day, friends!
    Recently, articles on the QtQuick \ QML topic about the Ubuntu SDK (based on QtQuick) and silence have completely stopped appearing on the hub; at the moment, this is the main toolkit offered for developing applications for Ubuntu (no less the most popular Linux- distribution kit). I wanted to the best of my ability to correct this situation by writing this article! It’s not worth trying to grasp the immensity, so I’ll start, perhaps, with a story about how I managed to replace a large amount of C ++ code with QML code (in the application under the Ubuntu SDK). If it became interesting to you, but it may also be incomprehensible, and here Yandex.Disk, then I ask for a cut!
    image

    Introduction

    I'll start from afar, but try briefly - a few years ago I wanted to create a client of some cloud storage for MeeGo (!). It so happened that it was at that moment that Yandex.Disk opened its API. I quickly implemented the WebDAV API service using C ++ \ Qt, and the GUI using QML. It turned out pretty well - a simple and reliable program, most of the reviews are positive (well, except for those who did not figure out how to log in = \).
    After some time, I decided to participate in the OpenSource development of basic applications for Ubuntu Phone - this is how I got acquainted with the Ubuntu SDK, working on the RSS Reader “Shorts”. Meanwhile, the Ubuntu App Showdown was approaching. I decided to participate with my client in the “Ported Applications” category (can be ported from any OS), since transferring the code from MeeGo to Ubuntu Phone is actually trivial. Failed to win for technical reasons. Nevertheless, the result was an excellent Yandex.Disk client for Ubuntu Phone. However, it also had a drawback - the C ++ part was built for ARM only, as a result, cross-platform was lost at the package level.
    And just recently I received a notification from Yandex about the release of the new Drive REST API in production. I immediately thought about implementing this API in pure JavaScript. For those who don’t know, QML (not very strictly speaking) includes JavaScript, that is, it allows you to use all the features of this language, in conjunction with the capabilities of the Qt library (properties, signals, etc., the result is quite powerful and flexible combination). The result would be a fully cross-platform implementation of the Yandex.Disk client (for all platforms where there is Qt, of course).

    Baseline and Goals

    So, there is a ready-made application that allows you to perform various operations on the contents of Yandex.Disk (copying, moving, deleting, receiving public links, etc.). The network part is implemented using C ++ \ Qt, as well as storing the model of the displayed data. The task is to switch to a new service API, implementing it already in JavaScript and without making changes to the UI code.
    image

    REST API implementation

    I have developed for myself a simple technique for implementing a web service API. It consists in using the extremely lightweight QtObject type with a custom set of properties and methods. Schematically, it looks like this:
    QtObject {
        id: yadApi
        signal responseReceived(var resObj, string code, int requestId)
        property string clientId: "2ad4de036f5e422c8b8d02a8df538a27"
        property string clientPass: ""
        property string accessToken: ""
        property int expiresIn: 0
        // Public methods...
        // Private methods...
    }
    

    The responseReceived signal is sent by the API every time an asynchronous response from XMLHttpRequest arrives (see below). The “accessToken” and “expiresIn” properties are set after authorization through OAuth from the outside (using the WebView on the login page for this task - it requests yadApi the URL to receive the token, clicks on it, prompts the user to enter their data, if successful, receives the token and his lifetime).
    And here is one of the public API methods - deleting a file:
    function remove(path, permanently) {
            if (!path)
                return
            var baseUrl = "https://cloud-api.yandex.net/v1/disk/resources?path=" + encodeURIComponent(path)
            if (permanently)
                baseUrl += "&permanently=true"
            return __makeRequst(baseUrl, "remove", "DELETE")
        }
    

    It is very simple - the request URL is formed from the passed parameters and then passed to the __makeReuqest internal method. It looks like this:
    function __makeRequst(request, code, method) {
            method = method || "GET"
            var doc = new XMLHttpRequest()
            var task = {"code" : code, "doc" : doc, "id" : __requestIdCounter++}
            doc.onreadystatechange = function() {
                if (doc.readyState === XMLHttpRequest.DONE) {
                    var resObj = {}
                    if (doc.status == 200) {
                        resObj.request = task
                        resObj.response = JSON.parse(__preProcessData(code, doc.responseText))
                    } else { // Error
                        resObj.request = task
                        resObj.isError = true
                        resObj.responseDetails = doc.statusText
                        resObj.responseStatus = doc.status
                    }
                    __emitSignal(resObj, code, doc.requestId)
                }
            }
            doc.open(method, request, true)
            doc.setRequestHeader("Authorization", "OAuth " + accessToken)
            doc.send()
            return task
        } 
    

    In the above piece of code, you can see the promised XMLHttpRequest, as well as sending a signal to get the result. In addition, a request object is generated - this is the operation code, identifier and XMLHttpRequest itself. In the future, it can be used to cancel, process the result, etc. If suddenly someone becomes interested about "__emitSignal" - it is implemented trivially:
    function __emitSignal(resObj, operationCode, requestId) {
            responseReceived(resObj, operationCode, requestId)
        }
    

    Such a code can be used to log and intercept the sending of signals. As for the internal function "__preProcessData" - it does nothing (!), It is a bookmark for the future. The fact is that in this regard I learned from bitter experience - when working with the Steam API, 64-bit numbers sometimes come in JSON'e of answers, though they are not enclosed in quotation marks. As a result, JavaScript perceives them as a double, accuracy is lost and long live sadness sorrow! The solution was the preprocessing of the input data, quotation of numbers, as well as subsequent work with them as with strings.
    And by and large it’s all - one by one, all the necessary API methods were implemented, namely creating a folder, copying, moving, deleting, loading, changing the status of publicity. In total, we got 140 (!) Lines of code in QML \ JS, which functionally completely replaced a thousand other lines of code in C ++ \ Qt of the WebDAV protocol implementation.

    Layer implementation

    The implementation of the WebDAV protocol in C ++ turned out to be quite simple and transparent, but it was inconvenient to use it directly from QML. In the old version, a special Bridge class was created as an intermediary (the name is a la KO), which makes it easier to work with the service. I decided not to abandon this approach in the new version and carefully replace my old Bridge with a new QML type of the same name with an identical set of methods and properties. To maintain its own API, so to speak, the UI would continue to call the same functions, but of a completely different entity. Again, schematically, it looks like this:
    QtObject {
        id: bridgeObject
        property string currentFolder: "/"
        property bool isBusy: taskCount > 0
        property int taskCount: 0
        property var tasks: []
        function slotMoveToFolder(folder) {
            if (isBusy)
                return
            // .... code
        }
        function slotDelete(entry) {
            __addTask(yadApi.remove(entry))
        }
        property QtObject yadApi: YadApi {
            id: yadApi
            onResponseReceived: {
                __removeTask(resObj.request)
                switch (resObj.request.code)
                {
                case "metadata":
                    // console.log(JSON.stringify(resObj))
                    if (!resObj.isError) {
                        var r = resObj.response
                        currentFolder = __checkPath(r.path)
                        // Filling model
                    } // !isError
                    break;
                case "move":
                case "copy":
                case "create":
                case "delete":
                case "publish":
                case "unpublish":
                    __addTask(yadApi.getMetaData(currentFolder))
                    break;
        } // API
        property ListModel folderModel: ListModel {
            id: dirModel
        }
    }
    

    So, to replace my own class, I needed the “currentFolder” and “isBusy” properties. The first property is used to store the current directory path for navigation. It is maintained up to date in the "slotMoveToFolder" method. Several properties and methods were added to account for executed requests (__addTask, __removeTask, the tasks array and its taskCount length. Just don’t have to be a QO now and say that the array has a length, and so - the property allows you to do bindings in QML, this case is used only in isBusy, in the future, somewhere else) I left the function naming as before - starting with the “slot” prefix (in the C ++ version of the class, you could achieve the visibility of methods from QML in two ways: make them slots or use Q_INVOKABLE). For brevity, again, I left only the method of deleting and moving to the specified directory, all the others are also present in the full version of the source code. Bridge methods are invoked directly from the UI.
    One of the features of the new Bridge is the implementation of the API described above - YadApi. Also, at the place of creation, listening to signals about the completion of the operation is performed with the implementation of the relevant actions. So, renaming or deleting, for example, causes a reload of the contents of the directory.
    Special attention is given to the data model - dirModel. In the previous implementation, I had the FolderModel class, which inherited from QAbstractItemModel according to the classical scenario - introducing my own roles (anyone familiar with Qt will understand at least a little what they are talking about) and so on. Now all of this has been easily abandoned in favor of the standard ListModel, which can store JS objects. This model is populated as follows:
    dirModel.clear()
    var items = r._embedded.items
    for(var i = 0; i < items.length; i++) {
        var itm = items[i]
        var o = {
            /* All entries attributes */
            "href" : __checkPath(itm.path),
            "isFolder" : itm.type == "dir",
            "displayName" : itm.name,
            "lastModif" : itm.modified,
            "creationDate" : itm.created,
            /* Custom attributes */
            "contentLen" : itm.size ? itm.size : 0,
            "contentType" : itm.mime_type ? itm.mime_type : "",
            "publicUrl" : itm.public_url ? itm.public_url : null,
            "publicKey" : itm.public_key ? itm.public_key : null,
            "isPublished" : itm.public_key ? true : false,
            "isSelected" : false,
            "preview" : itm.preview
        }
        dirModel.append(o)
    }
    

    Property names in the model also had to be left as in the old version for compatibility. This is not to say that in the C ++ implementation of the model I got a very big class, but getting rid of it using the standard model and such a small design is very nice!

    Conclusion

    In the end, I completely abandoned C ++ in my Yandex.Disk client. I in no way tend to the fact that there is something bad in the pluses or something like that. Not! The purpose of my article was to show the possibilities of pure QML - it can really be done a lot with it, although its primary task is to develop a UI (which is not actually covered in this article). And the code looks simple and clear , not at all like the implementation of a CSS calculator !
    Thanks for attention! Code can be found on launchpad'e .

    PSQuestions are welcome, if I wish I can reveal any part of the article in more detail!
    PSS In the next article I plan to address key aspects and tools of the Ubuntu SDK.

    Also popular now: