[C++] Implémentation de libcurl - Lire un fichier dont le nom est une variable

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous,

J’essaie actuellement d’implanter une fonction avec libcurl dont le seul et unique objectif est de télécharger un contenu JSON pour l’écrire dans un fichier.

Je me retrouve donc à mélanger C et C++ le temps de l’implémentation et à utiliser les fonctions de gestion de fichiers du langage C dont fopen.

Or, je me retrouve avec un drôle de comportement de la part de cette fonction fopen.

Voici le code :

size_t write_data(void* ptr, size_t size, size_t nmemb, FILE* stream){
    size_t written;
    written = fwrite(ptr, size, nmemb, stream);
    return written;
}

void HTTPDownloader::download(const char* url, const char* outputName){
    FILE *file;
    CURLcode res;
    curl = curl_easy_init();

    if(curl){
        if((file = fopen(outputName, "w+"))){
            curl_easy_setopt(curl, CURLOPT_URL, url);
            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
            res = curl_easy_perform(curl);
            fclose(file);
        }
        else{
            std::cout << "Failed to create/open the file " << outputName << std::endl;
        }
    }
}

void retrieveQueries(vector<pair<string, string>> qLoc){
    for(auto It = qLoc.begin(); It != qLoc.end(); It++){

        HTTPDownloader http;
        http.download(It->first.c_str(), It->second.c_str());
    }
}

Concernant It->first et It->second, ils contiennent respectivement un lien donnant accès à un query sur un serveur Prometheus et un nom de fichier dans lequel sera stocké le contenu JSON.

Dans cette configuration, le fichier ne s’ouvre pas ou n’est pas créé (sans le if, c’est une erreur de segmentation au niveau de fclose). En observant dans le débugger sous Qt Creator, file se retrouve avec un pointeur nul. Ce qui fait que le contenu du JSON ne peut pas être téléchargé.

En revanche, si j’initialise un array char contenant un random string comme ceci :

char outputName[FILENAME_MAX] = "data/swapfree.json";

…et que je le place dans la méthode download, alors fopen se comporte normalement en ouvrant/crééant le fichier avec en retour un pointeur non nul. Mais ce n’est malheureusement pas ce que je cherche à faire…

Que puis-je faire pour remédier à ce comportement ?

Merci d’avance !

EDIT: Il semble que je ne rencontre ce problème que sous QtCreator, en compilant et en faisant tourner le programme sur terminal (sous Manjaro), il s’exécute sans problème.

Terminal (Manjaro) : PASS, QtCreator : FAIL

+0 -0

Très certainement une histoire de droit.

QtCreator doit lancer le programme dans un lieu différent où il n’a pas les droits en écriture.

Vérifie la valeur de errno. Ou afficher directement le retour de perror.

+0 -0

Salut,

Je ne connais pas les environnements sous lesquels tu fais tourner ton programme. Par contre je constate dans ton code que tu crées un handle que tu passes ensuite à curl, pour qu’il aille lui écrire les données directement dans le fichier.

Du coup je suspecte un conflit de version de la bibliothèque standard. En plus clair, tu n’utilises pas le même jeu de fonctions fopen/fwrite/fclose que la bibliothèque curl, vous chargez des DLL/DSO différents. Les handles ne correspondent pas et donc comportement indéfini.

C’est un problème qui peut arriver si tu n’as pas compilé ton programme avec exactement le même compilateur que celui qui a été utilisé pour compiler curl.

Pour éviter ça, il faut garder en tête que celui qui construit un objet est le seul à pouvoir l’utiliser. Donc soit tu passes par un autre moyen pour récupérer les données, soit tu fais en sorte que ce soit curl qui se charge d’ouvrir/fermer le fichier.

Je ne connais pas curl par coeur mais il y a probablement un système de handle virtuel, tu passes à curl une fonction callback avec la même signature que fwrite et qui est censée avoir le même comportement; ainsi tu gardes le contrôle car c’est toi qui ouvre/ferme le fichier.

+0 -0

Je ne connais pas non plus :-), mais j’ai pu faire marcher un truc du genre (sous Linux)

#include <cstdlib>
#include <curl/curl.h>
#include <string>

class Loader {
public:
    void load(const std::string &url, const std::string &filename) {
        CURL *curl = curl_easy_init();

        if (curl) {
            curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
            FILE * output = fopen(filename.c_str(), "w+");
            curl_easy_setopt(curl, CURLOPT_WRITEDATA, output);

            CURLcode res = curl_easy_perform(curl);
            if (res != CURLE_OK) {
                fprintf(stderr, "curl_easy_perform() failed: %s\n",
                    curl_easy_strerror(res));
            }
            fclose(output);
            curl_easy_cleanup(curl);
        }
    }
};

int main(int argc, char** argv) {
    Loader loader;
    loader.load("http://www.google.fr", "/tmp/google.html");
    loader.load("http://www.labri.fr", "/tmp/labri.html");
    return (EXIT_SUCCESS);
}

Remarques

  • j’ai préféré traiter tout ce qui concerne l’interface avec C dans la classe Loader. C’est là que se fait la conversion string -> pointeur de caractère, ainsi que l’utilisation du FILE.
  • pour écrire dans un fichier, pas besoin d’écrire un callback, le callback par défaut sort le contenu dans le FILE* indiqué par le WRITEDATA. Lire la doc.
  • la doc, qu’on ne lit jamais assez, dit qu’il faut faire du ménage (cleanup) après le perform. C’est peut être la cause des soucis.
  • il ferait moins chaud, les erreurs lèveraient des exceptions au lieu de raconter leur vie. Je me suis contenté de transposer l’exemple de C à C++ https://curl.haxx.se/libcurl/c/simple.html
+0 -0

Merci à tous les trois pour vos réponses !

@ache: Je viens de récupérer les réponses de perror et errno que je viens de découvrir grâce à toi (un grand merci donc !).

PERROR: No such file or directory
ERRNO: Invalid argument

Autant la seconde ne me surprendrait pas, autant la première me semble étrange. Comme tu l’as dit, il y a peut-être des problèmes de droit, d’autant plus que sur terminal je ne rencontre aucun problème.

@QuentinC:

Je suis sous Manjaro Linux x64 et je travaillais au début sur VSCode + Terminal avant de tenter un passage sous QtCreator (essentiellement pour le debugger graphique).

Intéressant cette histoire, j’ai regardé un peu plus en détail dans la documentation et regardé cet exemple : libcurl example - fopen.c, ou bien celui-ci : libcurl example - url2file bien plus court et plus simple à mettre en oeuvre.

Pour revenir à cette histoire de compilateur, c’est une possibilité que je prends vraiment au sérieux. D’autant plus que comme je le disais plus haut, il n’y a que QtCreator qui visiblement n’a pas l’air d’aimer cela.

Oui, effectivement, il y a une fonction callback qui est envoyée à libcurl, cette fonction se trouve tout au début du code que j’ai posté. Je ne sais pas si c’est bien de ça dont tu veux parler, s’il s’agit d’autre chose, n’hésites pas.

@MichelBillaud :

Merci pour ta proposition !

Concernant le cleanup, je l’avais placé dans le destructeur de la classe HTTPDownloader (j’ai peut-être fait un mauvais choix) que j’ai oublié de mettre dans le post, que voici :

HTTPDownloader::~HTTPDownloader(){
    curl_easy_cleanup(curl);
}

EDIT: @MichelBillaud : J’ai oublié de te demander sur quel environnement tu as lancé ce programme, sous QtCreator?

+0 -0
  • à mon avis, dans ton code le curl_easy_init devrait être fait dans le constructeur, pas pour chaque perform. Symétrie avec le cleanup, qui irait dans le destructeur.
  • si ton downloader est fait pour servir plusieurs fois, faut le déclarer en dehors de la boucle.
  • L’IDE n’est absolument pour rien dans cette histoire. L’important c’est les outils de compilation, et les bibliothèques. Si tu veux tout savoir, c’est sous linux debian 10, compilateur gcc version 8.3.0 bibliothèque libcurl 4.
+0 -0

Merci à tous les trois pour vos réponses !

@ache: Je viens de récupérer les réponses de perror et errno que je viens de découvrir grâce à toi (un grand merci donc !).

PERROR: No such file or directory
ERRNO: Invalid argument

Autant la seconde ne me surprendrait pas, autant la première me semble étrange. Comme tu l’as dit, il y a peut-être des problèmes de droit, d’autant plus que sur terminal je ne rencontre aucun problème.

Palomino

Donc la valeur de outputName doit être de la forme dirs/fichier. Et au moins un dossier de dirs n’existe pas. fopen ne va pas les créer malheureusement.
Le problème peut venir du fait que QtCreator exécute le programme depuis un autre dossier.

Tu peux obtenir le dossier courant en C (oui je code en C pas en C++) avec la fonction getcwd (man 3 getcwd dans ton terminal). Si mon intuition est bonne, getcwd sera différent quand tu le lances depuis ton terminal ou depuis QtCreator.

+0 -0

@MichelBillaud:

  • Je prends note pour le curl_easy_init, c’est vrai que ça semble plus logique ainsi.
  • Je viens de me rendre compte effectivement que la déclaration du HTTPDownloader dans la boucle était un peu étrange, c’est corrigé.

@ache:

Effectivement, j’avais complètement oublié ce point. Je ne sais pas pourquoi, mais je me suis mis en tête que les dossiers allaient être crées, ce qui est assez étrange après coup.

Étant donné que je vais quand même avoir besoin de stocker ces fichiers dans un dossier, je vais mettre en oeuvre avec attention ce que tu viens de me proposer. Je verrai éventuellement si je ne trouve pas une alternative en C++.

Merci à vous trois en tout cas pour votre aide !

Le problème peut venir du fait que QtCreator exécute le programme depuis un autre dossier.

ache

C’est une erreur classique :

Il faut bien se mettre dans la tête que l’IDE n’est pas ce que l’utilisateur final emploiera pour lancer l’exécutable. Quand on l’exécute depuis un terminal, les chemins relatifs partent du répertoire courant du shell en cours d’exécution dans ce terminal.

Quand on teste un programme depuis un IDE, le répertoire courant est le plus souvent la racine du projet. En général, ça peut se régler dans les propriétés.

PS: pour récupérer le répertoire courant, on peut utiliser getcwd (on peut appeler toutes les fonctions C, de toutes façons), getenv("HOME") pour avoir le répertoire d’accueil.

+0 -0

Sinon, juste une remarque.

size_t write_data(void* ptr, size_t size, size_t nmemb, FILE* stream){
    size_t written;
    written = fwrite(ptr, size, nmemb, stream);
    return written;
}

C’est pas naturel ^^"

size_t write_data(void* ptr, size_t size, size_t nmemb, FILE* stream){
    return fwrite(ptr, size, nmemb, stream);
}

C’est plus naturel.

Mais du coup… Tu peux directement faire :

            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);

Sans déclarer une quelconque nouvelle fonction ^^

+0 -0

Mais du coup… Tu peux directement faire :

            curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fwrite);

Sans déclarer une quelconque nouvelle fonction ^^

ache

La fonction fwrite est déjà la "write function" par défaut, et il est donc inutile de positionner cette option. Juste passer le FILE* de sortie en CURLOPT_WRITEDATA. Du coup.

Voir la doc https://curl.haxx.se/libcurl/c/CURLOPT_WRITEDATA.html :

DESCRIPTION

A data pointer to pass to the write callback. If you use the CURLOPT_WRITEFUNCTION option, this is the pointer you’ll get in that callback’s 4th argument. If you don’t use a write callback, you must make pointer a 'FILE *' (cast to 'void *') as libcurl will pass this to fwrite(3) when writing data.

The internal CURLOPT_WRITEFUNCTION will write the data to the FILE * given with this option, or to stdout if this option hasn’t been set.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte