tomokei5634
@tomokei5634

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

node.jsでダウンローダーを作りたかったのですが...

解決したいこと

ひとつ、ひとつの関数の実行は可能なのに、まとめて実行するとエラーが出てしまいます。
解決方法をご教授いただけますと幸いです。

作りたいもの(仕様)

  • node.js(v16.3.0)を活用してダウンローダーを作ろうとしています。
  • ダウンロード対象のファイルのURLを記載したリストをテキストファイル.txtで作成します(以下、ダウンロードリストDL_url_list.txt
  • ダウンロードリストは、URLを1行ずつ、改行で記載していく仕様とします

ダウンロードリスト

DL_url_list.txt
https://i.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI
https://i.picsum.photos/id/107/200/300.jpg?grayscale&hmac=rlcVQxKrk3gxxjGVf4l4N-jX85wldhSIGczV9ZfeAL0
https://i.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U
  • 実行ファイルはapp.jsです
  • 実行ファイルとダウンロードリストは、同じ階層にあります
  • app.jsを実行すると、同階層に任意のディレクトリが作成されます(DL_dir)、それ以下のディレクトリ構造は、URLの構造に従うものとします。
URLが「https://i.picsum.photos/id/866/200/300.jpg」の場合

【ローカル構造】
app.js
DL_url_list.txt
DL_dir
  └ id
     └ 866
       └ 200
         └ 300.jpg

実行ファイル

app.js
// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
const createDir = (DirName) => {
  fs.mkdir(DirName, { recursive: true }, (err) => {
    if (err) console.log(err);
  });
  console.log("created - " + DirName);
};

// ダウンロードを実行
const fileDL = (Url, LocalPath) => {
  const output = fs.createWriteStream(LocalPath);
  https.get(Url, function (response) {
    response.pipe(output);
    response.on("end", function () {
      output.close();
      console.log("finished - " + Url);
    });
  });
};

// 以下、実行部
function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  createDir(DirName);
  fileDL(TargetUrl.href, LocalPath);
}

// ダウンロードリストに対して繰り返し実行
UrlList.forEach((target) => {
  download(target);
});

発生している問題・エラー

  • 上記の実行ファイルを、そのまま実行すると、エラーが出力されます
  • 「created - ***」と表示されていますが、実際はディレクトリは作成されていません
ターミナル
$ node -v
v16.3.0


$ node app.js

created - ./DL_dir/id/866/200
created - ./DL_dir/id/107/200
created - ./DL_dir/id/237/200
node:events:371
      throw er; // Unhandled 'error' event
      ^

Error: ENOENT: no such file or directory, open './DL_dir/id/866/200/300.jpg'
Emitted 'error' event on WriteStream instance at:
    at emitErrorNT (node:internal/streams/destroy:193:8)
    at emitErrorCloseNT (node:internal/streams/destroy:158:3)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: './DL_dir/id/866/200/300.jpg'
}

検証したこと

1)ダウンロード部の一時的除外

  • ダウンロードの実行部fileDL(TargetUrl.href, LocalPath);をコメントアウトして実行しました
  • すると、問題なくディレクトリの作成が実行されました
app.js
// 以下、実行部
function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  createDir(DirName);
  // fileDL(TargetUrl.href, LocalPath); // ← コメントアウト
}

2)ディレクトリ作成後の再実行

  • ディレクトリが作成された状態で、上記のコメントアウトを元へ戻し、再実行しました
  • すると、ダウンロードまで問題なく実行されました

image.png

検証からの推測

  • 「ディレクトリの作成createDir」と「ダウンロードの実施fileDL」の関数個々としては、実行できている
  • カレントディレクトリに、ダウンロード先のディレクトリ./DL_dirが存在しない場合で、createDirfileDLを一度に実行することでエラーが出力されることから、createDirfileDLを実行するタイミングの問題なのか...と思いましたが、解決に至っていません。

お力添えいただけますと幸いです。よろしくお願いいたします。
また「こういう作り方の方がいいよ」というアドバイスなどもいただけると、大変うれしいです。

0

10Answer

同期関数にしてもいいですが、Promiseもしくはasync/awaitの使い方を学べば後々できることが増えていいと思います。

3Like

Comments

  1. @tomokei5634

    Questioner

    @_y_s さん
    ありがとうございます!
    勉強になりました!
    特に「ブロッキングとノンブロッキングの概要」にあった次の一文が衝撃でした。
    「Node.js 標準ライブラリのすべての I/O メソッドは非同期バージョンを提供します。 これらはノンブロッキングで、コールバック関数を受け入れます。 一部のメソッドにはブロッキングに対応したものもあり、 その名前は Sync で終わります。」
    よく覚えておきます。
  2. @tomokei5634

    Questioner

    大変、お手数ですが、できましたら「Promiseもしくはasync/await」を使い非同期関数`fs.mkdir`のままでも実行可能なコードを、ご教授いただけませんでしょうか。

    色々と調べ持って、様々と試してみたのですが、いずれも上手くいかず....

    試したコードはコチラです
    https://qiita.com/tomokei5634/questions/f03670954532cc0e86ae#answer-4aa38eaf1096e9f3a6de

    ご教授いただけますと幸いです。よろしくお願いいたします。

@uasiさんも紹介していますが、fsのPromise版がありますので、そちらを使用した方が綺麗に書けます。

Promiseの利点

ここで、ちょっと話を戻しまして、同期と非同期(Promise)の違いを簡単に表すと下のような感じです。

同期(ブロッキング)
処理1: |----->|
処理2:        |--------->|
処理3:                   |--->|
※前の処理が終了したら次の処理を開始する
非同期(ノンブロッキング)
処理1: |----->|
処理2:  |--------->|
処理3:   |--->|
※前の処理を開始したら次の処理を開始する

つまり、時間のかかる処理が連続する場合もしくは時間のかかる処理中も他の処理をしたい場合は、非同期(Promise)を採用すべきです。

then()はPromiseの完了を待ってから実行してくれるメソッドですので、組み合わせると

非同期(ノンブロッキング)+then()
処理1: |----->|
処理2:  |--------->|
then :             |---->|
処理3:   |--->|
then :        |->|
all  :                   |------>|

というような時系列になります。※Promise.all()は指定した全てのPromiseを待機する

また、then()all()両メソッドともPromiseを返すのでメソッドチェーンでつなげることができます。(例:Promise.all().then().then().then()
このことによってかつてNode.jsで深刻だったコールバック地獄というものを回避することができます。

async/awaitについて

async/awaitPromiseのシンタックスシュガー(糖衣構文/構文糖)ですので、Promiseをマスターしてから使ってみた方がいいかもしれません。(なんとなく普通のコード(同期)と同じように書けてしまうので、問題が見えにくいです。)

注意点を紹介します。

  1. awaitはブロッキングです(then()を文にしたものに近い)。
  2. asyncは関数の返値Promiseでラップするものです。
  3. つまりawaitを含む関数はasync functionでなければいけませんが、async functionだからといってawaitは必須ではありません。
  4. awaitrejectをハンドリングするのには向きません(try-catch)。

(fs Promises APIを使わずに)手っ取り早く上のコードを改善するならば下のような感じになると思います。
※繰り返しですがfs Promises APIを使ったほう短く綺麗に書けます。
※確認していないので動かなかったらすみません。

function createDir(DirName) {
  return new Promise((resolve, reject) => {
    fs.mkdir(DirName, { recursive: true }, err => {
      if (err) {
        console.error(err);
        reject(err);
      } else {
        console.log("created - " + DirName);
        resolve(DirName); // 何か値を返した方が親切かもしれません。
      }
    });
  });
}

...

function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  return createDir(DirName).then(dir /* 返値を受け取って */ => {
    // この中でpathの処理ををした方がスッキリするかもしれません
    fileDL(TargetUrl.href, LocalPath);
  }, err => {
    // エラーハンドリング
    console.log("failed to create directory"); // とか
  });
}

// 「すべてダウンロードが終わったら」という処理をするならば
const allDLPromises = UrlList.map(target => download(target));
Promise.all(allDLPromises).then(() => {
  ...
});

最後にですが、命名規則について学ぶと見やすいコードが書けるようになると思います。(クラス以外でパスカルケースを使うことはほぼありません。)

2Like

Comments

これは実施していません(しなくても、エラーなく動いてしまい、私が正解を理解できていないため)

基本的にはnodeは全ての処理を終えてから終了するので、エラーにはなりません。
ただ、例えば「全てのファイルのダウンロードが終わったらログを出力する」というようなケースを考えると、Promise化しておくのが望ましいです。

一応の正答コードを載せておきます。細かく直していますが、できる限り原形を留めることを念頭に置いて修正しています。

  • まず、createDirにasync/awaitは不要です。async/awaitがどういう場合に役立つかは後ほど説明します。
  • エラー処理を簡単にですが一通り入れました。各所でのエラーを集約して、download実行時に処理しています。
  • 全てのダウンロードが終了したらログにメッセージを出力するようにしました(Promise.all)。
"use strict";

// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");
const { resolve } = require("path/posix");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
function createDir(DirName) {
    return new Promise((resolve, reject) => {
        fs.mkdir(DirName, { recursive: true }, (err) => {
            if (err) {
                reject(err);
            } else {
                console.log("created - " + DirName);
                resolve();
            }
        });
    });
}

// ダウンロードを実行
function fileDL(Url, LocalPath) {
    const output = fs.createWriteStream(LocalPath);
    return new Promise((resolve, reject) => {
        https.get(Url, (response) => {
            response.pipe(output);
            response.on("end", function () {
                output.close();
                console.log("finished - " + Url);
                resolve();
            });
        }).on("error", (err) => {
            reject(err);
        });
    });
}

// 以下、実行部
function download(target) {
    const TargetUrl = new URL(target);
    const PathName = TargetUrl.pathname;
    const DirName = DLBaseDir + path.dirname(PathName);
    const LocalPath = DLBaseDir + PathName;
    return createDir(DirName).then(() => {
        return fileDL(TargetUrl.href, LocalPath);
    });
}

const promiseList = UrlList.map((target) => {
    return download(target).catch((err) => {
        console.error(err);
    });
});

Promise.all(promiseList).then(() => {
    console.log("completed!");
});

コールバック

例えばこれをコールバックで書くと、以下のようになります。各関数でエラー処理がごちゃついているのと、download関数の中が複雑化しているのが分かると思います。download関数の中のように、コールバックが何重にも重なっていく様子をコールバックヘル(コールバック地獄)と呼びます。今回は二重で処理も少ないのでまだ見れますが、三重四重で処理も増えてくると…。

また、「全てのダウンロードが終了したらログにメッセージを出力する」はコールバックだけではできません。こういった処理を待ち合わせ処理と言って、非同期処理では必須なのですが、コールバックはEventEmitter等の他の技術と組み合わせなければ待ち合わせ処理ができません。

こういったところが嫌われて、代替技術が模索された結果、Promiseにたどり着いた、というのが現状です。

"use strict";

// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");
const { resolve } = require("path/posix");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
function createDir(DirName, cb) {
        fs.mkdir(DirName, { recursive: true }, (err) => {
            if (err) {
                cb(err);
                return;
            }
            console.log("created - " + DirName);
            cb(null);
        });
}

// ダウンロードを実行
function fileDL(Url, LocalPath, cb) {
    const output = fs.createWriteStream(LocalPath);
        https.get(Url, (response) => {
            response.pipe(output);
            response.on("end", function () {
                output.close();
                console.log("finished - " + Url);
                cb(null);
            });
        }).on("error", (err) => {
            cb(err);
        });
}

// 以下、実行部
function download(target, cb) {
    const TargetUrl = new URL(target);
    const PathName = TargetUrl.pathname;
    const DirName = DLBaseDir + path.dirname(PathName);
    const LocalPath = DLBaseDir + PathName;
    createDir(DirName, (err) => {
        if (err) {
            cb(err);
            return;    
        }
        fileDL(TargetUrl.href, LocalPath, (err) => {
            if (err) {
                cb(err);
                return;
            }
            cb(null);
        });       
    });
}

UrlList.forEach((target) => {
    download(target, (err) => {
        console.error(err);
    });
});

// console.log("completed!"); // (コールバックだけでは)出来ない

async/await

asyncとawaitはセットで使いますが、関数の中でawaitを使いたいときにその関数をasync関数にするのが基本です。今回async関数にする意味があるのは、downloadぐらいです。thenをawaitに置き換えることで、まるで同期処理であるかのように非同期処理が書けます。今回のコードには出てきませんが、「返り値を同期処理と同じように受け取れる」「同期処理と同じようにtry/catchができる」といったこともawaitの特徴で、つまりは「同期処理と同じように非同期処理を書ける」というのがasync/awaitの要点です。
ただ、便利な反面、Promiseをすべてasync/awaitに置き換えればいいわけではありません。async関数は「Promiseを返す関数」であり、awaitは「Promiseの処理を待ち合わせる」ものなので、常にPromiseは付きまといます。また、Promise.allのように、async/awaitで表現できない処理は、生のPromiseを触る必要が出てきます。

// 以下、実行部
async function download(target) {
    const TargetUrl = new URL(target);
    const PathName = TargetUrl.pathname;
    const DirName = DLBaseDir + path.dirname(PathName);
    const LocalPath = DLBaseDir + PathName;
    await createDir(DirName);
    await fileDL(TargetUrl.href, LocalPath);
}
2Like

Comments

  1. @tomokei5634

    Questioner

    @masaha03 さん
    ありがとうございます!

    正答コードと、コールバックの比較、とっても、とってもありがたいです!
    文面を拝見して理解できた気になるのですが、「じゃあ書いてみて」と言われると・・・

    しっかりと各書き方を比較して、理解にしたいと思います。

fs.mkdir は非同期関数のため、ディレクトリを作り終える前に createDir を抜けてしまっています。 fs.mkdirSync を使ってください。

1Like

Comments

  1. @tomokei5634

    Questioner

    @usai さん
    ありがとうございます!
    `fs.mkdirSync`を使うと、すんなりと実行できました!
    大変、勉強になりました!

@usai さんに教えていただいた方法を備忘録として記載します。

app.js
// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
const createDir = (DirName) => {
  fs.mkdirSync(DirName, { recursive: true }); // [fs.mkdir] から [fs.mkdirSync] へ変更
  console.log("created - " + DirName);
};

// ダウンロードを実行
const fileDL = (Url, LocalPath) => {
  const output = fs.createWriteStream(LocalPath);
  https.get(Url, function (response) {
    response.pipe(output);
    response.on("end", function () {
      output.close();
      console.log("finished - " + Url);
    });
  });
};

// 以下、実行部
function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  createDir(DirName);
  fileDL(TargetUrl.href, LocalPath);
}

UrlList.forEach((target) => {
  download(target);
});

fs.mkdir(path[, options], callback)

Asynchronously creates a directory.
The callback is given a possible exception and, if recursive is true, the first directory path created, (err, [path]). path can still be undefined when recursive is true, if no directory was created.
非同期にディレクトリを作成します。
コールバックには可能性のある例外が与えられ、recursiveが真の場合は、最初に作成されたディレクトリのパスが(err, [path])となります。

fs.mkdirSync(path[, options])

Synchronously creates a directory. Returns undefined, or if recursive is true, the first directory path created. This is the synchronous version of fs.mkdir().
ディレクトリを同期的に作成します。未定義、または recursive が真であれば最初に作成されたディレクトリパスを返します。これは、fs.mkdir()の同期版です。

1Like

@tomokei5634 さん、 createDir を async 関数にしただけでは fs.mkdir の完了を待つようにはなりません。最初の質問のコードと同様、ディレクトリの作成を待たずに処理が先に進んでしまいます。

Node.js v14 またはそれ以降なら Promise を返すバージョンの API である fsPromises.mkdir が使えます。それを呼んで、返り値の Promise を await してください。

const fsPromises = require("fs/promises");

async function createDir(DirName) {
  await fsPromises.mkdir(DirName, { recursive: true }).catch((err) => {
    console.log(err);
  });
  console.log("created - " + DirName);
  return true;
}

それ以前のバージョンなら fs.mkdir のコールバックを待つ Promise でラップし、それを await する必要があります。

async function createDir(DirName) {
  await new Promise((resolve, reject) => {
    fs.mkdir(DirName, { recursive: true }, (err) => {
      if (err) {
        console.log(err);
        reject();
      else {
        resolve();
      }
    });
  });
  console.log("created - " + DirName);
  return true;
}

今回はエラーになっていませんが、 fileDL の中の https.get も同様に Promise でラップして await しなければなりません。

このような Promise の使い方については、 @_y_s さんが回答された資料や https://jsprimer.net/basic/async/#promise を参照してください。

ちなみに、 async/await のコードの中で -Sync 系の API を呼ぶことも一応は可能です。他の非同期処理をブロックしてしまうのでおすすめはしませんが。

1Like

Comments

  1. @tomokei5634

    Questioner

    @uasi さん、本当にありがとうございます。
    おっしゃる通りにすると、動きました。が、理解が追いついておらず...

    https://qiita.com/tomokei5634/questions/f03670954532cc0e86ae#answer-57a812576623a25884d3

    とくに「Promise でラップし、それを await」という部分と、「fileDL の中の https.get も同様に」という部分が...

    今日一日、非同期ばっかり見てましたが、まだまだ理解できていないようです。

    勉強します。ありがとうございます。
  2. まず fs.mkdir のコールバックについて説明します。

    fs.mkdir(DirName, { recursive: true }, (err) => { ... });

    は、「コールバック関数 (err) => { ... } を渡しておくから、ディレクトリを作り終わったら呼んでね」と fs.mkdir にお願いしているわけです。ディレクトリを作るのは一瞬かもしれないし10秒かかるかもしれません。 fs.mkdir は「非同期にやっておくから先に進んでいいよ」と返り値なしですぐ終了します。

    fs.mkdir が終了するとそれ以降のコードが実行されますが、その時点ではまだディレクトリを作り終わっていないかもしれず、ディレクトリがあることを期待するコードがエラーを出すかもしれません。なんとかしてディレクトリを作り終わるまで、つまりコールバックが呼ばれるまで待ちたいですね。

    ここで Promise と async/await が役に立ちます。Promise とは何かしらの非同期処理を表すオブジェクトです。たとえば fsPromises.mkdir はディレクトリ作成処理を表す Promise を返します。 Promise には処理が完了したかどうかを問い合わせることができます。

    コールバックで完了を伝える fs.mkdir のような関数は一種の非同期処理であり、 Promise で包むことができます。 new Promise((resolve, reject) => { ... }) という形で Promise を作り、 ... の位置にラップしたい関数を入れます。その関数に渡すコールバックの中で resolve() (エラーを返したい場合は reject())を呼ぶと Promise が完了したことになります。

    async/await は Promise を使いやすくするための構文です。「await 《Promise を返す式》」と書くとその Promise が完了するまで待つような動作をします。待つと言っても裏では隙を見て他の非同期処理も実行してくれるので、プログラム全体が固まることはありません。

    「Promise でラップし、それを await」とは、 fs.mkdir による非同期処理を包んだ Promise を作って、それが完了するまで待つということです。

    https.get も fs.mkdir と同様にコールバックで完了を伝える関数なので、 Promise で包んだうえで完了を待たなければなりません。

@_y_s さんにご教授いただいたことを念頭にコードを書き直しました

  • fs.mkdirhttps.getをPromiseでラップしたバージョンです。
  • 変数部の記述をパスカルケースからキャメルケースへ修正しました。
app.js
"use strict";

// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
function createDir(dirName) {
  return new Promise((resolve, reject) => {
    fs.mkdir(dirName, { recursive: true }, (err) => {
      if (err) {
        reject(err);
      } else {
        resolve(dirName);
      }
    });
  });
}

// ダウンロードを実行
function fileDL(Url, localPath) {
  return new Promise((resolve, reject) => {
    https
      .get(Url, (response) => {
        const output = fs.createWriteStream(localPath);
        response.pipe(output);
        response.on("end", () => {
          output.close();
          resolve(Url);
        });
      })
      .on("error", (err) => {
        reject(err);
      });
  });
}

// 以下、実行部
function download(target) {
  const targetUrl = new URL(target);
  const pathName = targetUrl.pathname;
  const dirName = DLBaseDir + path.dirname(pathName);
  const localPath = DLBaseDir + pathName;
  return createDir(dirName).then(
    (dir) => {
      console.log("created - " + dir);
      return fileDL(targetUrl.href, localPath).then(
        (url) => {
          console.log("finished - " + url);
        },
        (err) => {
          console.log("failed to download" + err);
        }
      );
    },
    (err) => {
      console.log("failed to create directory" + err);
    }
  );
}

const allDLPromises = UrlList.map((target) => download(target));
Promise.all(allDLPromises).then(() => {
  console.log("end");
});
1Like

@_y_s さん のご提言にチャレンジしてみました

(「Promiseもしくはasync/await」を使い非同期関数fs.mkdirのままでも実行可能なコードの作成にチャレンジしました)

が、上手くいきませんでした。

↓※このコードではエラーがでます(Error: ENOENT: no such file or directory

app.js
"use strict";

// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");
const { resolve } = require("path/posix");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
async function createDir(DirName) {
  fs.mkdir(DirName, { recursive: true }, (err) => {
    if (err) console.log(err);
  });
  console.log("created - " + DirName);
  return true;
}

// ダウンロードを実行
async function fileDL(Url, LocalPath) {
  const output = fs.createWriteStream(LocalPath);
  https.get(Url, (response) => {
    response.pipe(output);
    response.on("end", function () {
      output.close();
      console.log("finished - " + Url);
    });
  });
  return true;
}

// 以下、実行部
async function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  await createDir(DirName);
  await fileDL(TargetUrl.href, LocalPath);
}

UrlList.forEach((target) => {
  download(target);
});

何がいけないのでしょうか...?

fs.mkdir()fs.mkdirSync()へ変えることで、問題なく機能はするので、冗長な相談となりますが、ご教授いただけますと幸いです。よろしくお願いいたします。

0Like

@uasi さんのご提言により修正したコードです。

今回はエラーになっていませんが、 fileDL の中の https.get も同様に Promise でラップして await しなければなりません。

これは実施していません(しなくても、エラーなく動いてしまい、私が正解を理解できていないため)
「いちおう動くコード」として残します。

app.js
"use strict";

// モジュールの読み込み
const https = require("https");
const fs = require("fs");
const url = require("url");
const path = require("path");
const { resolve } = require("path/posix");

// 設定1:ダウンロードするURLのリスト(.txt)を指定
const UrlList = fs.readFileSync("./DL_url_list.txt", "utf-8").toString().split("\n");
// 設定2:ローカルのダウンロード先のディレクトリを指定
const DLBaseDir = "./DL_dir";

// ローカルのダウンロード先のディレクトリを作成
async function createDir(DirName) {
  await new Promise((resolve, reject) => {
    fs.mkdir(DirName, { recursive: true }, (err) => {
      if (err) {
        console.log(err);
        reject();
      } else {
        console.log("created - " + DirName);
        resolve();
      }
    });
  });
}

// ダウンロードを実行
function fileDL(Url, LocalPath) {
  const output = fs.createWriteStream(LocalPath);
  https.get(Url, (response) => {
    response.pipe(output);
    response.on("end", function () {
      output.close();
      console.log("finished - " + Url);
    });
  });
}

// 以下、実行部
function download(target) {
  const TargetUrl = new URL(target);
  const PathName = TargetUrl.pathname;
  const DirName = DLBaseDir + path.dirname(PathName);
  const LocalPath = DLBaseDir + PathName;
  createDir(DirName).then(() => {
    fileDL(TargetUrl.href, LocalPath);
  });
}

UrlList.forEach((target) => {
  download(target);
});

0Like

@_y_s さん
@uasi さん
@masaha03 さん

本当に、本当にありがとうございます。
心から感謝申し上げます。

これを機にしっかりとPromisesを勉強しようと思いました。
ご教授いただいた内容を再度見直し、しっかり咀嚼して落とし込む所存です。

本当にありがとうございました。

0Like

Your answer might help someone💌