Source: gdfs.js

/*global gapi:false*/
"use strict";
const debug = require("debug")("gdfs");
debug("loading");
const GdfsEvent = require("./gdfs-event.js");
const GdfsPath = require("./gdfs-path.js");

/**
 * Gdfs class is an interface for the Google Drive API v3.
 *
 * The instance manages a current working directory(CWD) and offers methods
 * to operate files and folders on the Google Drive by its pathname.
 *
 * Before creating an instance, the APIs must be loaded by the class method
 * [`loadApi`](#.loadApi) with a ClientId and ApiKey.
 * These had to be created in a project of Google devloper Console.
 *
 * And to operates files, user must sign-in with the Google account.
 * See [signIn](#.signIn) and [signOut](#.signOut).
 *
 * Instance's CWD is initialized to the root on constructor. It can be changed
 * by [chdir](#chdir) method. When it is changed, the 'oncwdupdate' callback
 * is fired. To know where the CWD is, The [cwd](#cwd) method is available.
 *
 * @constructor
 */
function Gdfs() {
    this._oncwdupdate = null;
    this._currentPath = [{ id: "root", name: "", }];
}

/**
 * Create Gdfs client.
 * @returns {Gdfs} The google drive interface that has a current directory.
 */
Gdfs.createClient = () => {
    return new Gdfs();
};

/**
 * signInStatusChangeEvent
 * @type {GdfsEvent}
 */
Gdfs.signInStatusChangeEvent = new GdfsEvent(
    window, "gdfs-signin-status-change");

/**
 * Load Google Drive APIs and initialize its client object.
 *
 * The loaded all APIs are accessible with a global `gapi` object.
 * But it is wrapped by this class so the users should not use it directly.
 *
 * @param {string} clientId A clientId from the Developer console.
 * @param {string} clientSecret An clientSecret from the Developer console.
 * @returns {Promise} A promise that will be resolved when the loading completed.
 */
Gdfs.loadApi = (clientId, clientSecret) => {
    debug("Start of Gdfs.loadApi");
    const script = document.createElement("SCRIPT");
    script.setAttribute("async", "async");
    script.setAttribute("src", "https://apis.google.com/js/api.js");
    const p = new Promise( (resolve, reject) => {
        script.addEventListener("load", () => {
            script.onload = () => {};
            gapi.load("client:auth2", async () => {
                debug("initialize gapi.client");

                if(typeof(clientId) === "object" && clientSecret == null &&
                    "clientId" in clientId && "clientSecret" in clientId &&
                    "discoveryDocs" in clientId && "scope" in clientId)
                {
                    await gapi.client.init(clientId);
                } else {
                    await gapi.client.init({
                        clientId, clientSecret,
                        discoveryDocs: [
                            "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"
                        ],
                        scope: [
                            "https://www.googleapis.com/auth/drive",
                            "https://www.googleapis.com/auth/drive.appdata",
                            "https://www.googleapis.com/auth/drive.file",
                            "https://www.googleapis.com/auth/drive.metadata",
                            "https://www.googleapis.com/auth/drive.metadata.readonly",
                            "https://www.googleapis.com/auth/drive.photos.readonly",
                            "https://www.googleapis.com/auth/drive.readonly",
                        ].join(" "),
                    });
                }
                gapi.auth2.getAuthInstance().isSignedIn.listen( () => {
                    debug("the signed-in-status changed");
                    Gdfs.signInStatusChangeEvent.fire();
                });
                Gdfs.signInStatusChangeEvent.fire();
                debug(`Gdfs.loadApi SignedIn: ${Gdfs.isSignedIn()}`);
                debug("Gdfs.loadApi is resolved");
                resolve();
            });
        });
        script.addEventListener("readystatechange", () => {
            debug(`readystatechange ${script.readyState}`);
            if(script.readyState === "complete") {
                script.onload();
            }
        });
        script.onerror = event => {
            debug("Gdfs.loadApi is rejected");
            reject(new URIError(
                `The script ${event.target.src} is not accessible.`));
        };
        document.body.appendChild(script);
    });
    debug("End of Gdfs.loadApi");
    return p;
};

/**
 * A mime type of the Google Drive's folder.
 * @type {string}
 */
Gdfs.mimeTypeFolder = "application/vnd.google-apps.folder";

/**
 * Check if gapi was signed in.
 * @returns {boolean} true if gapi is signed in, otherwise false.
 */
Gdfs.isSignedIn = () => {
    return gapi.auth2.getAuthInstance().isSignedIn.get();
};

/**
 * Sign in to Google Drive.
 * @async
 * @returns {undefined}
 */
Gdfs.signIn = async () => {
    return await gapi.auth2.getAuthInstance().signIn();
};

/**
 * Sign out from the Google Drive.
 * @async
 * @returns {undefined}
 */
Gdfs.signOut = async () => {
    return await gapi.auth2.getAuthInstance().signOut();
};

/**
 * Get file list.
 * @async
 * @param {object} queryParameters The parameters for the API.
 * @returns {Promise<object>} The result of the API.
 */
Gdfs.getFileList = async (queryParameters) => {
    const response = await gapi.client.drive.files.list(queryParameters);
    return response.result;
};

/**
 * Find a folder by name from a folder.
 * @async
 * @param {string} parentFolderId A parent folder id.
 * @param {string} folderName A folder name to find
 * @returns {Array<object>} A folder list that found.
 */
Gdfs.findFolderByName = async (parentFolderId, folderName) => {
    debug("No tests pass: Gdfs.findFolderByName");

    const folders = [];
    const q = [
        `parents in '${parentFolderId}'`,
        `name = '${folderName}'`,
        `mimeType = '${Gdfs.mimeTypeFolder}'`,
        "trashed = false",
    ].join(" and ");

    const params = {
        "pageSize": 10,
        "pageToken": null,
        "q": q,
        "fields": "nextPageToken, " +
                  "files(id, name, mimeType, webContentLink, webViewLink)",
    };

    debug(`${JSON.stringify(params)}`);
    try {
        do {
            const result = await Gdfs.getFileList(params);
            debug(`${JSON.stringify(result)}`);
            for(const file of result.files) {
                folders.push(file);
            }
            params.pageToken = result.nextPageToken;
        } while(params.pageToken != null);
    } catch(err) {
        debug(err.stack);
    }

    return folders;
};

/**
 * Find a file by name from a folder.
 * @async
 * @param {string} parentFolderId A parent folder id.
 * @param {string} fileName A file name to find
 * @returns {Promise<Array<object> >} A folder list that found.
 */
Gdfs.findFileByName = async (parentFolderId, fileName) => {
    debug("No tests pass: Gdfs.findFileByName");

    const files = [];
    const q = [
        `parents in '${parentFolderId}'`,
        `name = '${fileName}'`,
        "trashed = false",
    ].join(" and ");

    const params = {
        "pageSize": 10,
        "pageToken": null,
        "q": q,
        "fields": "nextPageToken, " +
                  "files(id, name, mimeType, webContentLink, webViewLink)",
    };

    debug(`findFileByName: params: ${JSON.stringify(params, null, "  ")}`);
    do {
        const result = await Gdfs.getFileList(params);
        for(const file of result.files) {
            debug(`findFileByName: found file: ${JSON.stringify(file)}`);
            files.push(file);
        }
        debug(`findFileByName: result.nextPageToken: ${result.nextPageToken}`);
        params.pageToken = result.nextPageToken;
    } while(params.pageToken != null);

    return files;
};

/**
 * Get file resource.
 * @async
 * @param {object} queryParameters The parameters for the API.
 * @returns {Promise<object>} The result of the API.
 */
Gdfs.getFileResource = async (parameters) => {
    const response = await gapi.client.drive.files.get(parameters);
    return response.result;
};

/**
 * Check if the file is a folder.
 * @param {object} file The file object provided from the result
 * of `getFileList` method.
 * @returns {boolean} The file is a folder or not.
 */
Gdfs.isFolder = (file) => {
    return file.mimeType === Gdfs.mimeTypeFolder;
};

/**
 * Get a file content as text from Google Drive.
 * Even if the file is not a text actually, it could be converted
 * to ArrayBuffer, Blob or JSON to use by Web App.
 * @param {string} fileId The file id to download.
 * @param {boolean|null} acknowledgeAbuse A user acknowledgment
 * status for the potential to abuse. This parameter is optional.
 * default value is false.
 * @returns {Promise<string>} A downloaded content as text.
 */
Gdfs.downloadFile = (fileId, acknowledgeAbuse) => {
    return requestWithAuth("GET",
        "https://www.googleapis.com/drive/v3/files/"+fileId,
        { alt: "media", acknowledgeAbuse : acknowledgeAbuse });
};

/**
 * Create a new file's resource.
 * @param {string} folderId The folder id where the file is created.
 * @param {string} filename The file name.
 * @param {string} mimeType The mime type for the new file.
 * @returns {Promise<object>} The response of the API.
 */
Gdfs.createFile = async (folderId, filename, mimeType) => {
    const response = await requestWithAuth("POST",
        "https://www.googleapis.com/drive/v3/files", {},
        { "Content-Type": "application/json", },
        JSON.stringify({
            name: filename,
            mimeType: mimeType,
            parents: [folderId],
        }));
    return JSON.parse(response);
};

/**
 * Upload a file content to update a existing file.
 * @param {string} fileId The file id to update.
 * @param {string} mimeType The content type of the file.
 * @param {any} data The file content.
 * @returns {Promise<object>} The response of the API.
 */
Gdfs.updateFile = async (fileId, mimeType, data) => {
    const response = await requestWithAuth("PATCH",
        "https://www.googleapis.com/upload/drive/v3/files/"+fileId,
        { uploadType: "media" },
        { "Content-Type": mimeType },
        data);
    return JSON.parse(response);
};

/**
 * @param {string} method The request method.
 * @param {string} endpoint The endpoint of API.
 * @param {object} queryParams The query parameters.
 * @param {object} headers The request headers.
 * @param {any} body The request body.
 * @returns {Promise<object>} The response of the request.
 */
const requestWithAuth = (method, endpoint, queryParams, headers, body) => {
    let xhr = new XMLHttpRequest();
    xhr.open(method, createUrl(endpoint, queryParams), true);
    headers = headers || {};
    Object.keys(headers).forEach( name => {
        xhr.setRequestHeader(name, headers[name]);
    });
    xhr.setRequestHeader("Authorization",
        "Bearer " + getAccessToken());
    xhr.timeout = 30000;
    return new Promise( (resolve, reject) => {
        xhr.onload = () => { resolve(xhr.responseText); };
        xhr.onerror = () => { reject(new Error(xhr.statusText)); };
        xhr.ontimeout = () => { reject(new Error("request timeout")); };
        xhr.send(body);
    });
};

/**
 * Get access-token on current session.
 * @returns {string} The access token.
 */
const getAccessToken = () => {
    const googleUser = gapi.auth2.getAuthInstance().currentUser.get();
    const authResponse = googleUser.getAuthResponse(true);
    const accessToken = authResponse.access_token;
    return accessToken;
};

/**
 * Create URI including query parameters.
 * @param {string} endpoint The endpoint of API.
 * @param {object|null} params The query parameters.
 * @returns {string} The URI.
 */
const createUrl = (endpoint, params) => {
    if(params == null) {
        return endpoint;
    }
    let keys = Object.keys(params).filter(
        key => (key !== ""));
    if(keys.length == 0) {
        return endpoint;
    }
    let queryString = keys.map( key => {
        let value = params[key];
        return (value == null ? null : `${key}=${encodeURI(value)}`);
    }).join("&");
    return `${endpoint}?${queryString}`;
};

/**
 * Get actual root folder id.
 * @async
 * @return {Promise<string>} The root folder's id
 */
Gdfs.getActualRootFolderId = async () => {
    const res = await Gdfs.getFileResource({ fileId: "root", fields: "id" });
    debug(`getActualRootFolderId: res ${JSON.stringify(res, null, "  ")}`);
    return res.id;
};

/**
 * Set oncwdchage callback hander.
 * @param {FUnction|AsyncFunction} handler a function to be invoked when
 *      the current directory is changed.
 * @returns {undefined|Function} the previous handler will be returned.
 */
Gdfs.prototype.onCwdUpdate = function(handler) {
    const prev = this._oncwdupdate;
    if(handler != null) {
        this._oncwdupdate = handler;
    }
    return prev;
};

/**
 * Fire cwdUpdate.
 * @returns {Promise} what the handler returns.
 */
Gdfs.prototype.fireCwdUpdate = async function() {
    if(this._oncwdupdate) {
        try {
            const result = this._oncwdupdate();
            if(result != null) {
                if(result.constructor === Promise) {
                    return await result;
                }
                return result;
            }
        } catch (err) {
            debug(err.stack);
        }
    }
};

/**
 * Get current folder id.
 * @returns {string} The folder id that the instance is.
 */
Gdfs.prototype.getCurrentFolderId = function() {
    return this._currentPath.slice(-1)[0].id;
};

/**
 * Get current working directory as path object.
 * @returns {GdfsPath} the current working directory.
 */
Gdfs.prototype.getCurrentPath = function() {
    const path = this._currentPath.map( path => `${path.name}/`).join("");
    const cwd = new GdfsPath(path);
    debug(`getCurrentPath: ${cwd.toString()}`);
    return cwd;
};

/**
 * Set current working directory with path object.
 * @async
 * @param {GdfsPath} path the new current working directory.
 * @returns {Promise<boolean>} the status of the operation.
 */
Gdfs.prototype.setCurrentPath = async function(path) {
    debug("No tests pass: Gdfs#setCurrentPath");
    debug(`setCurrentPath(${path})`);
    if(!path.isAbsolute()) {
        debug(`The path must be absolute. ${path}`);
        return false;
    }
    if(!(await this.isDirectory(path))) {
        debug(`${path} is not a directory`);
        return false;
    }
    this._currentPath = await this.getPaths(path);
    await this.fireCwdUpdate();
    return true;
};

/**
 * Get an array of path element from root directory.
 * @async
 * @param {GdfsPath} path path object.
 * @returns {Promise<Array<object> >} the array of the object having an id and
 *      the name.
 */
Gdfs.prototype.getPaths = async function(path) {
    debug("No tests pass: Gdfs#getPaths");
    debug(`getPaths(${path})`);
    if(!path.isAbsolute()) {
        debug("getPaths: Error: the path must be absolute");
        return null;
    }
    const paths = [ { id:"root", name:"", mimeType: Gdfs.mimeTypeFolder } ];
    for(const name of path.elements().slice(1)) {
        if(name === "") {
            break;
        }
        const parent = paths.slice(-1)[0];
        debug(`name: ${name}, parent: ${JSON.stringify(parent)}`);
        const path = { id: null, name: null, mimeType: null };
        if(parent.id != null) {
            const children = await Gdfs.findFileByName(parent.id, name);
            if(children.length > 0) {
                const child = children.shift();
                path.id = child.id;
                path.name = child.name;
                path.mimeType = child.mimeType;
            }
        }
        paths.push(path);
    }
    debug(`getPaths: ${JSON.stringify(paths, null, "  ")}`);
    return paths;
};

/**
 * Get the file object that the path points to.
 * @param {GdfsPath} path the path.
 * @returns {file} the file object of google drive.
 */
Gdfs.prototype.getFileOfPath = async function(path) {
    const paths = await this.getPaths(path);
    if(!paths) {
        return null;
    }
    return paths.slice(-1)[0];
};

/**
 * Get the current working directory of gdrive-fs.
 * @returns {string} The current working directory.
 */
Gdfs.prototype.cwd = function() {
    return this.getCurrentPath().toString();
};

/**
 * Changes the current working directory of this client session.
 * @param {string} directory A pathname to operate.
 * @async
 * @returns {Promise<boolean>} the status of the operation.
 */
Gdfs.prototype.chdir = async function(directory) {
    debug("No tests pass: Gdfs#chdir");
    const cwd = this.getCurrentPath();
    const next_cwd = GdfsPath.merge(cwd, new GdfsPath(directory));
    return await this.setCurrentPath(next_cwd);
};

/**
 * Move current directory to root, parent or one of children.
 * @async
 * @param {string} folderId A destination file id to move.
 *      To move to parent, ".." is available.
 * @returns {Promise<boolean>} the status of the operation.
 */
Gdfs.prototype.chdirById = async function(folderId) {
    debug(`Gdfs.chdirById( ${folderId} )`);
    if(folderId === ".") {
        return true;
    }
    const currentFolderId = this.getCurrentFolderId();
    if(folderId === "/" || folderId === "root" ) {
        this._currentPath = [ { id:"root", name:"" } ];
        await this.fireCwdUpdate();
    } else if(folderId === "..") {
        if(currentFolderId === "root") {
            debug("Could not move to upper folder from root.");
            return false;
        }
        this._currentPath.pop();
        await this.fireCwdUpdate();
    } else if(folderId !== currentFolderId) {
        const paths = [];
        const root = await Gdfs.getFileResource({fileId: "root", fields: "id"});
        let searchId = folderId;
        for(;;) {
            const file = await Gdfs.getFileResource({
                fileId: searchId,
                fields: "id, name, parents, mimeType",
            });
            if(file == null) {
                debug(`folder ${searchId} is not found.`);
                return false;
            }
            if(file.mimeType !== Gdfs.mimeTypeFolder) {
                debug(`folder ${searchId} is not folder.`);
                return false;
            }
            debug(JSON.stringify(file, null,  "  "));
            if(file.id == root.id) {
                paths.unshift({id: "root", name: "" });
                break;
            } else {
                paths.unshift({id: file.id, name: file.name });
            }
            searchId = file.parents.shift();
        }
        debug(JSON.stringify(paths, null, "  "));
        this._currentPath = paths;
        await this.fireCwdUpdate();
    }
    return true;
};

/**
 * Check the path is a directory.
 * @async
 * @param {GdfsPath} path A path to check
 * @returns {Promise<Boolean>} The path is a directory or not.
 */
Gdfs.prototype.isDirectory = async function(path) {
    debug("No tests pass: Gdfs#isDirectory");
    const file = await this.getFileOfPath(this.toAbsolutePath(path));
    if(!file) {
        return false;
    }
    return file.mimeType === Gdfs.mimeTypeFolder;
};

/**
 * Convert to absolute path.
 * @param {GdfsPath} path path to be converted
 * @returns {GdfsPath} An absolute path
 */
Gdfs.prototype.toAbsolutePath = function(path) {
    debug("No tests pass: Gdfs#toAbsolutePath");
    if(path.isAbsolute()) {
        return path;
    }
    const cwd = this.getCurrentPath();
    return GdfsPath.merge(cwd, path);
};

/**
 * Read the directory to get a list of filenames.
 *
 * This method may not returns all files in the directory.
 * To know all files were listed, check the `pageToken` field in the parameter
 * `options` after the invocation.
 * If the reading was completed, the field would be set `null`.
 * The rest files unread will be returned at the next invocation with same
 * parameters.
 *
 * ```javascript
 * const readDirAll = async path => {
 *     const opts = { pageSize: 10, pageToken: null };
 *     const files = [];
 *     do {
 *        for(const fn of await files.readdir(path, opts)) {
 *            files.push(fn);
 *        }
 *     } while(opts.pageToken != null);
 * };
 * ```
 *
 * @async
 * @since v1.1.0
 * @param {string} path A path to the directory.
 *
 * @param {object|null} options (Optional) options for this method.
 *
 * Only two fields are available:
 *
 * * "pageSize": Set maximum array size that this method returns at one
 * time.  The default value 10 will be used if this is not specified or
 * zero or negative value is specified.
 * * "pageToken": Set null to initial invocation to read from first
 * entry. This would be updated other value if the unread files are
 * remained. The value is used for reading next files. User should not
 * set the value except for null.
 *
 * If this parameter is ommited, all files will be read.
 * This is not recomended feature for the directory that has a number of files.
 *
 * @returns {Promise<Array<string> >} returns an array of filenames.
 */
Gdfs.prototype.readdir = async function(path, options) {
    path += path.match(/\/$/) ? "" : "/";
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`readdir: parentFolder: ${JSON.stringify(parentFolder)}`);

    if(!parentFolder || parentFolder.id == null) {
        debug(`readdir: The path not exists ${path}`);
        return null;
    }

    if(!Gdfs.isFolder(parentFolder)) {
        debug(`readdir: The path is not a folder ${path}`);
        return null;
    }

    const files = [];
    const readAll = (options == null);
    options = options || {};
    const pageSize = options.pageSize || 10;
    let pageToken = options.pageToken || null;

    const readFiles = async params => {
        debug(`readdir: params: ${JSON.stringify(params, null, "  ")}`);
        const result = await Gdfs.getFileList(params);
        debug(`readdir: result.nextPageToken: ${result.nextPageToken}`);
        for(const file of result.files) {
            files.push(file.name);
        }
        return result.nextPageToken;
    };

    const params = {
        "pageSize": pageSize <= 0 ? 10 : pageSize,
        "pageToken": pageToken,
        "q": `parents in '${parentFolder.id}' and trashed = false`,
        "fields": "nextPageToken, files(name)",
    };

    if(!readAll) {
        // eslint-disable-next-line require-atomic-updates
        options.pageToken = await readFiles(params);
    } else {
        do {
            // eslint-disable-next-line require-atomic-updates
            options.pageToken = await readFiles(params);
        } while(options.pageToken != null);
    }

    debug(`readdir: files: ${JSON.stringify(files)}`);
    return files;
};

/**
 * Get file's properties.
 * It is a file resource of Google Drive including id, name, mimeType,
 * webContentLink and webViewLink about the file or directory.
 *
 * @async
 * @param {string} path A pathname.
 * @returns {File} The file resource of Google Drive including id, name,
 *      mimeType, webContentLink and webViewLink about the file or directory.
 * @since v1.1.0
 */
Gdfs.prototype.stat = async function(path) {
    debug(`Gdfs#stat(${path})`);
    path = path.replace(/\/+$/, "");
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    debug(`stat: absPath: ${absPath.toString()}`);
    path = absPath.toString();
    if(path === "/") {
        const file = await Gdfs.getFileResource({
            fileId: "root",
            fields: "id, name, mimeType, webContentLink, webViewLink",
        });
        debug(`stat: file ${JSON.stringify(file)}`);
        return file;
    }
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`stat: parentFolder: ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`stat: The path not exists ${path}`);
        return null;
    }
    const filename = absPath.getFilename();
    debug(`stat: filename: ${filename}`);
    const files = await Gdfs.findFileByName(parentFolder.id, filename);
    if(files.length === 0) {
        debug(`stat: File not found ${path}`);
        return null;
    }
    const file = files.shift();
    debug(`stat: file ${JSON.stringify(file)}`);
    return file;
};

/**
 * Read a file.
 * The file must have webContentLink in its resource to read the contents,
 * To get the resource, Use [`Gdfs#stat`](#stat).
 *
 * @async
 * @param {string} path A pathname to operate.
 * @returns {Promise<string>} The file content.
 */
Gdfs.prototype.readFile = async function(path) {
    debug(`Gdfs#readFile(${path})`);
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`readFile: parentFolder: ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`readFile: The path not exists ${path}`);
        return null;
    }
    const filename = absPath.getFilename();
    debug(`readFile: filename: ${filename}`);
    const files = await Gdfs.findFileByName(parentFolder.id, filename);
    debug(`readFile: files: ${JSON.stringify(files)}`);
    if(files.length === 0) {
        debug(`File not found ${path}`);
        return null;
    }
    const file = files.shift();
    if(!file.webContentLink) {
        debug(`File is not downloadable ${path}`);
        return null;
    }
    return await Gdfs.downloadFile(file.id);
};

/**
 * Make a directory.
 * @async
 * @param {string} path A pathname to operate.
 * @returns {Promise<object>} The API response.
 */
Gdfs.prototype.mkdir = async function(path) {
    debug(`mkdir(${path})`);

    path = path.replace(/\/+$/, "");
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`mkdir: parentFolder ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`mkdir: The path not exists ${path}`);
        return null;
    }
    const pathname = absPath.getFilename();
    debug(`mkdir: pathname: ${pathname}`);
    const files = await Gdfs.findFileByName(parentFolder.id, pathname);
    debug(`mkdir: files: ${JSON.stringify(files)}`);
    if(files.length > 0) {
        debug(`mkdir: The directory exists ${path}`);
        return null;
    }
    const result = await Gdfs.createFile(
        parentFolder.id, pathname, Gdfs.mimeTypeFolder);
    if(parentFolder.id === this.getCurrentFolderId()) {
        await this.fireCwdUpdate();
    }
    return result;
};

/**
 * Remove the directory but not a normal file.
 * The operation will fail, if it is not a directory nor empty.
 * @async
 * @param {string} path A pathname to operate.
 * @returns {Promise<object|null>} Returns the API response.
 *      null means it was failed.
 */
Gdfs.prototype.rmdir = async function(path) {
    debug(`rmdir(${path})`);
    path = path.replace(/\/+$/, "");
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`rmdir: parentFolder ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`rmdir: The path not exists ${path}`);
        return null;
    }
    const pathname = absPath.getFilename();
    debug(`rmdir: pathname: ${pathname}`);
    if(pathname === "") {
        debug(`rmdir: The root directory cannot be removed ${path}`);
        return null;
    }
    const dires = await Gdfs.findFolderByName(parentFolder.id, pathname);
    debug(`rmdir: dires: ${JSON.stringify(dires)}`);
    if(dires.length === 0) {
        debug(`rmdir: The directory not exists ${path}`);
        return null;
    }
    const dir = dires.shift();
    debug(`rmdir: dir ${JSON.stringify(dir)}`);
    debug(`rmdir: _currentPath ${JSON.stringify(this._currentPath, null, "  ")}`);
    if(this._currentPath.filter(parent => parent.id == dir.id).length > 0 ||
        dir.id === await Gdfs.getActualRootFolderId())
    {
        debug(`rmdir: The path is a parent ${path}`);
        return null;
    }
    if(dir.mimeType !== Gdfs.mimeTypeFolder) {
        debug(`rmdir: The path is not folder ${path}`);
        return null;
    }
    const params = {
        "q": `parents in '${dir.id}' and trashed = false`,
        "fields": "files(id)",
    };
    debug(`rmdir: params ${JSON.stringify(params)}`);
    const children = await Gdfs.getFileList(params);
    debug(`rmdir: children: ${JSON.stringify(children, null, "  ")}`);
    if(children.files.length > 0) {
        debug(`rmdir: The folder is not empty ${path}`);
        return null;
    }
    const response = await gapi.client.drive.files.delete(
        { fileId: dir.id });
    if(parentFolder.id === this.getCurrentFolderId()) {
        await this.fireCwdUpdate();
    }
    return response.result;
};

/**
 * Delete the file but not directory.
 * This does not move the file to the trash-box.
 *
 * @async
 * @param {string} path A pathname to operate.
 * @returns {Promise<object|null>} Returns the API response.
 *      null means it was failed.
 */
Gdfs.prototype.unlink = async function(path) {
    debug(`unlink(${path})`);
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`unlink: parentFolder ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`unlink: The path not exists ${path}`);
        return null;
    }
    const pathname = absPath.getFilename();
    debug(`unlink: pathname: ${pathname}`);
    const files = await Gdfs.findFileByName(parentFolder.id, pathname);
    debug(`unlink: files: ${JSON.stringify(files)}`);
    if(files.length === 0) {
        debug(`unlink: The file not exists ${path}`);
        return null;
    }
    const file = files.shift();
    if(file.mimeType === Gdfs.mimeTypeFolder) {
        debug(`unlink: The file is a folder ${path}`);
        return null;
    }
    const response = await gapi.client.drive.files.delete({ fileId: file.id });
    const result = response.result;
    if(parentFolder.id === this.getCurrentFolderId()) {
        await this.fireCwdUpdate();
    }
    return result;
};

/**
 * Write a file.
 * @async
 * @param {string} path A pathname to operate.
 * @param {string} mimeType A mimeType of the file content.
 * @param {string} data A file content.
 * @returns {Promise<object>} The API response.
 */
Gdfs.prototype.writeFile = async function(path, mimeType, data) {
    debug(`Gdfs#writeFile(${path},${mimeType}, ${JSON.stringify(data)})`);
    const absPath = this.toAbsolutePath(new GdfsPath(path));
    const parentFolder = await this.getFileOfPath(absPath.getPathPart());
    debug(`writeFile: parentFolder: ${JSON.stringify(parentFolder)}`);
    if(!parentFolder || parentFolder.id == null) {
        debug(`writeFile: The path not exists ${path}`);
        return null;
    }
    const filename = absPath.getFilename();
    debug(`writeFile: filename: ${filename}`);
    const files = await Gdfs.findFileByName(parentFolder.id, filename);
    debug(`writeFile: files: ${JSON.stringify(files)}`);
    if(files.length === 0) {
        const file = await Gdfs.createFile(
            parentFolder.id, filename, mimeType);
        const result = await Gdfs.updateFile(file.id, mimeType, data);
        if(parentFolder.id === this.getCurrentFolderId()) {
            await this.fireCwdUpdate();
        }
        return result;
    }
    const file = files.shift();
    if(file.mimeType === Gdfs.mimeTypeFolder) {
        debug(`writeFile: The path already exists as directory ${path}`);
        return null;
    }
    const result = await Gdfs.updateFile(file.id, mimeType, data);
    if(parentFolder.id === this.getCurrentFolderId()) {
        await this.fireCwdUpdate();
    }
    return result;
};

module.exports = Gdfs;