Статьи

Создание API-оболочки в Phone Gap

Вчера я работал над оберткой PhoneGap Build API для Node. Это уже существует — https://github.com/germallon/phonegapbuildapi  — но я подумал, что это будет забавный эксперимент. API сборки  является  реальным  просто так я предполагал , мой код будет также довольно просто. Это было … сначала.

Я начал с чтения разделов API. Это позволяет получать приложения, значки, загружать файлы и т. Д. PhoneGap позволяет запрашивать токен или передавать аутентификационную информацию при каждом обращении. Я выбрал легкий путь и просто проходил аутентификацию с каждым запросом. Мне удалось обернуть большую часть логики для всех вызовов API с помощью двух служебных функций:

function getConfig(path) {
    return {
        auth: username + ":" + password,
        host:"build.phonegap.com",
        port:"443",
        path:"/api/v1/"+path
    }
}

//I handle doing the config get, http, string contact, etc
function doCall(path, success, fail) {
    var options = getConfig(path);
    var req = http.get(options, function(res) {
        var resultString = "";
        res.on("data", function(c) {
            resultString+=c;
        });
    
        res.on("end",function() {
            var result = JSON.parse(resultString);
            success(result);
        });
    }).on("error", function(e) {
        if(fail) fail(e);
    }); 

Основной функцией здесь является doCall, который ожидает путь к API. Все вызовы API имеют один и тот же базовый URL, поэтому я упростил его, просто передав необходимую дополнительную часть. HTTP-вызовы в Node немного сложнее, чем CF, потому что они асинхронные, но не сложные. Вы, вероятно, можете догадаться, что здесь происходит. Я открываю запрос, который получает объект результата. У результирующего объекта есть событие данных, что означает, что поток дерьма включен. Я добавляю его к переменной. Есть конечное событие, которое запускает — как вы уже догадались — в конце. Затем я могу просто проанализировать результат и запустить обработчик успеха.

В качестве примера, вот API для получения всех приложений: 

    doCall("apps", function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res.apps);
    },function(e) {
        if(fail) fail(e);
    });

 И, наконец, вот как приложение Node может использовать его:

pgbuild = require("./pgbuild");

pgbuild.setUsername("[email protected]");
pgbuild.setPassword("isitmillertimeyet?");

//Test getting all the apps
pgbuild.getAllApps(function(apps) {
    console.log("I got lots of apps! How many? "+apps.length);
    //console.dir(apps);
}, function(e) {
    console.log("Oh snap, an error");
    console.dir(e);

 Большая часть кода, который я написал для API чтения, следует этому формату — запрашивать дерьмо и передавать обработчик успеха / неудачи.

Так что, как я уже сказал, все было довольно просто. Я думаю, что я сгорел через API чтения около 30 минут или около того. Это были забавные единороги и радуги. Затем я начал работать над API записи и попал в кирпичную стену. Зачем? API записи, или, вернее, та часть, над которой я работал — создание приложений, — позволяет загружать файлы при определении нового приложения. Вы также можете указать новые приложения в хранилище, но сначала я хотел поработать над версией файла. (Назовите меня обжорой для наказания — я знал, что это будет проблемой.) Оказывается, загрузка файлов — главная проблема  в тылу . Как в — нет никакой реальной встроенной поддержки в основных библиотеках Node.js. Googling было очень сложно, а так как почти каждый результат был о  обработке  загрузить файл, не  делая запрос на загрузку файла.

После еще более яростного поиска в Google (и небольшого перерыва в Diablo 3) я нашел этот  пост . Я бы с удовольствием назвал этого парня или девчонку, но на его странице «О нас» не говорится, кто он или она. Поэтому я решил, что этот человек …

… только потому, что он / она / это было чертовски полезно. Я включил некоторые из его логики в мой окончательный код, и хотя я не очень доволен созданным коллажем, он работает правильно. Вот пример вызова: 

    create_method:"file",
    file:"./forupload/index.html"
    }, function(res) {
        console.log("Ok in making an app");
        console.dir(res);
    }, function(e) {
        console.log("I got an error: ");
        console.dir(e);
    }
);
view rawgistfile1.js

 И вот он на блестящем новом сайте PhoneGap Build …

Интересно — браузеры значительно упростили загрузку файлов в JavaScript с помощью XHR2. Если вы еще не видели этого в действии, ознакомьтесь с отличной   статьей HTML5 Rocks .

Для тех, кто хочет поиграть с этим, я включил весь код pgbuild.js ниже. Помните — я пишу Node.js около недели — так что, если вы используете это в работе, у вас есть мое восхищение.

var http = require("https");
var fs = require("fs");
var path = require("path");

var username = "";
var password = "";

exports.setUsername = function(u) { username = u; }
exports.setPassword = function(p) { password = p; }

exports.createApp = function(options, success, fail) {
    var httpOptions = getConfig("apps");
    httpOptions.method = "POST";
    
    //Detect if options.create_method is file, and if so, suck in the bits
    //Fails if no .file
    //Also note it doesn't support .zip yet.
    if(options.create_method === "file") {
        if(!options.file) throw new Error("Must supply file value.");
        console.log("Need to read in a file:"+options.file);
        //Shell out for file uploads
        PreparePost(httpOptions,JSON.stringify(options), options.file, success);
    } else {
        //TODO
    }
    
}

exports.getAllApps = function(success,fail) {
    doCall("apps", function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res.apps);
    },function(e) {
        if(fail) fail(e);
    });
}

exports.getApp = function(id, success, fail) {
    doCall("apps/"+id, function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res);
    },function(e) {
        if(fail) fail(e);
    });
}

exports.getAppIcon = function(id, success, fail) {
    doCall("apps/"+id +"/icon", function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res.location);
    },function(e) {
        if(fail) fail(e);
    });
}

//todo: Possibly validate platform? Should be: android,blackberry,ios,symbian,webos,winphone
exports.getAppDownload = function(id, platform, success, fail) {
    doCall("apps/"+id +"/"+platform, function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res.location);
    },function(e) {
        if(fail) fail(e);
    });
}

exports.getKeys = function() {
    var platform = "";
    if(arguments.length == 1) {
        success = arguments[0];
    } else if(arguments.length === 2) {
        success = arguments[0];
        fail = arguments[1];
    } else if(arguments.length == 3) {
        platform = arguments[0];
        success = arguments[1];
        fail = arguments[2];
    } 
    
    var path = "keys";
    if(platform != "") path+="/"+platform;
    
    doCall(path, function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res.keys);
    },function(e) {
        if(fail) fail(e);
    });
}

exports.getKey = function(platform, id, success, fail) {
    doCall("keys/"+platform +"/"+id, function(res) {
        if(res.error && res.error.length && fail) fail(res.error);
        else success(res);
    },function(e) {
        if(fail) fail(e);
    });
}

function getConfig(path) {
    return {
        auth: username + ":" + password,
        host:"build.phonegap.com",
        port:"443",
        path:"/api/v1/"+path
    }
}

//I handle doing the config get, http, string contact, etc
function doCall(path, success, fail) {
    var options = getConfig(path);
    var req = http.get(options, function(res) {
        var resultString = "";
        res.on("data", function(c) {
            resultString+=c;
        });
    
        res.on("end",function() {
            var result = JSON.parse(resultString);
            success(result);
        });
    }).on("error", function(e) {
        if(fail) fail(e);
    });
}



//CREDIT: http://onteria.wordpress.com/2011/05/30/multipartform-data-uploads-using-node-js-and-http-request/
//Note that I modified his code quite a bit

//For file uploads
function EncodeFieldPart(boundary,name,value) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n";
    return_part += value + "\r\n";
    return return_part;
}

function EncodeFilePart(boundary,type,name,filename) {
    var return_part = "--" + boundary + "\r\n";
    return_part += "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + filename + "\"\r\n";
    return_part += "Content-Type: " + type + "\r\n\r\n";
    return return_part;
}

//I expect the config options, the JSON data string, and file path
function PreparePost(httpOptions,data,file,success) {
    var boundary = Math.random();
    var post_data = [];

    post_data.push(new Buffer(EncodeFieldPart(boundary, 'data', data), 'ascii'));
    post_data.push(new Buffer(EncodeFilePart(boundary, 'text/plain', 'file', path.basename(file)), 'ascii'));

    var contents = fs.readFileSync(file, "ascii");
    post_data.push(new Buffer(contents, "utf8"));
    post_data.push(new Buffer("\r\n--" + boundary + "--"), 'ascii');
    
    MakePost(httpOptions,post_data, boundary,success);
}

function MakePost(httpOptions,post_data, boundary,success) {

    var length = 0;
    
    for(var i = 0; i < post_data.length; i++) {
        length += post_data[i].length;
    }

    httpOptions.headers = {
        'Content-Type' : 'multipart/form-data; boundary=' + boundary,
        'Content-Length' : length
    };

    var post_request = http.request(httpOptions, function(response){
        response.setEncoding('utf8');
        var res="";
        response.on('data', function(chunk){
            res+=chunk;
        });
        response.on('end',function() {
            success(JSON.parse(res));
        });
    });

    for (var i = 0; i < post_data.length; i++) {
        post_request.write(post_data[i]);
    }
    post_request.end();