📜 ⬆️ ⬇️

About QML and the new Yandex.Disk REST API

Good day, friends!
Recently, articles on QtQuick \ QML About Ubuntu SDK (based on QtQuick) have completely ceased to appear on Habré and silence, but at the moment it is the main toolkit offered for developing applications for Ubuntu (the most popular Linux distribution kit). I wanted to correct this situation to the best of my ability by writing this article! It’s not worth trying to embrace the immense, so I’ll start, perhaps, with the story of how I managed to replace a large amount of code with C ++ code with QML (in the application under the Ubuntu SDK). If it became interesting to you, and maybe even incomprehensible, and where is Yandex.Disk, then I ask for the cat!
image

Introduction

I will start from afar, but I will try briefly - a few years ago I wanted to create a client of some cloud storage under MeeGo (!). It so happened that at that very moment Yandex.Disk opened its API. I quickly implemented the WebDAV API service using C ++ \ Qt, and 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 met the Ubuntu SDK while 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 (you can port from any OS), the benefit of transferring the code from MeeGo to Ubuntu Phone is actually trivial. It was not possible to win for technical reasons. However, the result was an excellent Yandex.Disk client under Ubuntu Phone. However, he also had a drawback - the C ++ part was assembled under ARM only, as a result, at the package level, the cross-platform was lost.
And most recently, I received a notification from Yandex about the release of a new REST API Disk in production. I immediately thought about implementing this API in pure JavaScript. For those who do not know - QML (not particularly 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 Qt is available, of course).

Baseline and Objectives

So, there is a ready-made application that allows you to perform various operations on the contents of Yandex.Disk (copy, move, delete, retrieve public links, etc.). The network part is implemented using C ++ \ Qt, as well as the storage of the display data model. The task is to switch to a new API of the service, having already implemented it in JavaScript and not making revisions in the UI code.
image

REST API implementation

I developed for myself a simple technique for implementing a web service API. It consists in using the extremely lightweight type QtObject 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 object each time an asynchronous response comes from XMLHttpRequest (see below). The “accessToken” and “expiresIn” properties are set after passing authorization through OAuth from the outside (on the login page for this task, WebView is used - it requests the URL from yadApi to get the token, follows it, prompts the user to enter their data, if successful his life time).
And here is one of the public API methods - file deletion:
 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 __maReqqest 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 receive the result. In addition, a request object is formed - it is an operation code, an identifier, and the XMLHttpRequest itself. Later it can be used for cancellation, processing of the result, etc. If suddenly someone becomes interested in "__emitSignal" - it is implemented trivially:
 function __emitSignal(resObj, operationCode, requestId) { responseReceived(resObj, operationCode, requestId) } 

This code can be used to log and intercept sending signals. As for the internal function "__preProcessData" - it does not do anything (!), It is a bookmark for the future. The fact is that I have learned from bitter experience in this regard - when working with the Steam API, 64-bit numbers sometimes come up in the JSON of responses, moreover, they are not enclosed in quotes. As a result, JavaScript perceives them as double, accuracy is lost, and long live sadness, sadness! The solution was the preprocessing of incoming data, enclosing numbers in quotes, as well as the subsequent work with them already as with strings.
And by and large this is all - one by one all the API methods I needed were implemented, namely creating a folder, copying, moving, deleting, loading, changing the status of publicity. In total, there were 140 (!) Lines of code in QML \ JS, which functionally completely replaced one thousand other lines of code with C ++ \ Qt implementations of the WebDAV protocol.
')
Implementation of the layer

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 class Bridge (a la KO) was created as an intermediary, which makes it easier to work with the service. I decided not to abandon this approach in the new version and neatly replace my old Bridge with a new, similarly named QML type with an identical set of methods and properties. To support the API, so to speak, UI would continue to cause the same functions, but absolutely other 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 when navigating. It is kept up-to-date in the “slotMoveToFolder” method. We also added several properties and methods for taking into account running queries (__addTask, __removeTask, the tasks array and its taskCount length. Just don’t have to be CO now and say that the array has a length and so the property allows you to make binding 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, methods from QML could be made visible 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 rest are also present in the full version of the source code. Methods of type Bridge are called directly from the UI.
One of the properties 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 execution of appropriate actions. So, renaming or deleting, for example, causes a reload of the directory contents.
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 classic scenario - introducing my own roles (those who are familiar with Qt will understand a little about what they mean) and so on. Now, all of this was 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. I can’t say that in the C ++ model implementation I got a very large class, but it’s very pleasant to get rid of it using the standard model and such a small structure!

Conclusion

In the end, I completely abandoned C ++ in my Yandex.Disk client. I do not in any way mean that there is something bad or like that in the pros. Not! The purpose of my article was to show the capabilities of pure QML — it can really do a lot with its help, although its primary task is to develop a UI (in this article, it is not actually affected). And the code looks simple and clear , not at all like the implementation of a calculator on CSS !
Thanks for attention! The code can be found on launchpad'e .

PS Questions are welcome, if you wish I can disclose any part of the article in more detail!
PSS In the next article I plan to touch on the key aspects and tools of the Ubuntu SDK.

Source: https://habr.com/ru/post/230435/


All Articles