20211123のNode.jsに関する記事は3件です。

Nodejs: multerでファイルアップロードし、sharpでファイルをリサイズして、S3へファイルをアップロード

背景 写真アプリを作っていて、一覧画面を作ったが、ファイルサイズが大きくて表示に時間がかかってしまう。そのため、オリジナルファイルの表示ではなく、サムネールを表示し、選択したもののみをダウンロード表示するような仕組みに変更することにした。そこで、サムネールの作る必要があったため、そのメモとなる。 仕組み的には、Multerでファイルをアップロードし、アップロードしたファイルをSharpでリサイズして、私が利用しているS3へオリジナルファイルとサムネールの両方をアップロードする仕組み。 Multer Multer自体は上記の説明が詳しいですが、私は単一ファイルのアップロードを利用して、特にStorageなども使ってはない。 RoutesでMulterを定義した。 route.js // ファイルをアップロードし、一時保管するフォルダを定義 var multer = require('multer'); var upload = multer({ dest: 'uploads/', limits: { fieldSize: 25 * 1024 * 1024 } }) // Multerをミドルウェアとして利用しているコードの部分だけ抜粋。 router.post('/:albumid/upload', authenticateJWT, upload.single('formtest'), PicturesController.uploadNewPicture); 上記のように定義して、実際私とControllerにてアップロードしたファイルを参照したかったですが、 少し上記の文章と異なる部分があった。 req.fileでは、undefinedになっている。 そのため、req.fileではなく、req.body.fileを利用した。 Controller.js // 上記の参照した文章では、下記のコードでファイルを参照していたが、私はUndefinedになっている。 file = req.file // 私は下記のようにreqのbodyからファイルを取得することができた。 file = req.body.file Sharp 上記はSharpの公式ドキュメントである。 私が直面したエラーが一つあり、下記である。 Error: Input file contains unsupported image format 私はフロントエンドでFileReaderを利用して、ファイルを取得している。 最初は全然気にしていなかったが、下記のサイトで下記のようなコメントがある。 The blob's result cannot be directly decoded as Base64 without first removing the Data-URL declaration preceding the Base64-encoded data. To retrieve only the Base64 encoded string, first remove data:/;base64, from the result. Data-URL宣言を削除してから、base64でエンコードしたファイルになるようです。 そのため下記のような変換が必要となる。 Conntroller.js // req.body.fileをそのままSharpのinputとして入力するとエラーになる。 // Error: Input file contains unsupported image format imagefile = Buffer.from(req.body.file.replace(/^data:image\/\w+;base64,/, ""),'base64'); sharp(imagefile) .resize(200,200, { // リサイズする時のオプション設定、詳しくは上記の公式サイトへ // アスペクト比を維持しながら、画像のサイズをできるだけ大きくする fit: sharp.fit.inside, // ファイルサイズが指定のサイズより低ければ変更しない withoutEnlargement: true }) .toFormat('jpeg') .toBuffer() .then(function(outputBuffer){ // Upload image file to S3 // 実際、オリジナルファイルとサムネールを両方S3へアップロードしている。 // imagefile と outputbufferをs3.putObjectを利用してアップロードすれば良い。 } S3 次いでにS3へファイルをアップロードするコードも添付。 s3method.js // Setting up S3 upload parameters const params = { Bucket: process.env.MyPhotoBucket, Key: s3key, ContentType: 'image/jpeg', ContentEncoding: 'base64', Body: bufferfile }; // Uploading files to the bucket var s3response = await s3.putObject(params, function(err, data) { if (err) { console.log("error happend when upload file to s3"); console.log(err); throw err; } console.log(`File uploaded successfully. ${data}`); }).promise(); 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsはシングルスレッド、排他は必要ない

※タイトルは釣りです。 この記事はNode.js初心者がasync/await実装をマルチスレッドと誤解して 混乱するのを解消するために書きました。 Node.jsのコードはシングルスレッドで動きます。 (厳密に言うとWorkerとかモジュール内部とかマルチスレッドな部分はありますが、ユーザーが普通に書くコードとしては) 結論 あなたがasyncと書いても、コードが2箇所で同時に実行されることはありません。 実行されるコードは常に1箇所のみです。 awaitと書くとコードの実行が止まって、他のコードに処理が移ることがあります。 awaitではないコードで、処理が他のコードに切り替わることはありません。 あなたが作りたいものによって、排他が必要になることがあります。 質問 外部から通信を受け付けるプログラムを書いています。 通信Aを処理している最中に、通信Bが届いたとき、何が起こりますか? 「通信Aの処理が終わるまで、通信Bの受信処理は実行されない」 「通信Aの処理の最中に、通信Bの受信処理が割り込む」 回答 (作り方によって)どっちもありうる awaitの処理が割り込まれない例 //時間がかかる処理 async function heavyOperation(){ console.log('heavyOperation'); let x = 0; for(let i=0;i<10000000000;i++){ x += i / 10000000000; } } async function callback(){ console.log('callback'); } async function exec(){ console.log('exec'); setTimeout(callback,1); const startTime = Date.now(); console.log('heavy operation started.'); await heavyOperation(); console.log('heavy operation finished.', Date.now() - startTime, 'ms'); } exec(); % node settimeout.js exec heavy operation started. heavyOperation heavy operation finished. 12254 ms callback heavyOperationの実行には12秒ほどかかっていますが、 setTimeout(1ミリ秒)によるcallback実行は後回しにされました。 関数callbackにasyncがついている/ついていないで違いはありません。 awaitの処理が割り込まれる例 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function callback(){ console.log('callback'); } async function exec(){ console.log('exec'); setTimeout(callback,1); const startTime = Date.now(); console.log('sleep started.'); await sleep(3000); console.log('sleep finished.', Date.now() - startTime, 'ms'); } exec(); % node sleep.js exec sleep started. callback sleep finished. 3000 ms await sleep(1000ミリ秒)の最中にsetTimeoutによる関数callbackの呼び出しが行われています。 つまりawaitする関数の中身によって、他の処理に切り替わるかどうかが変わります。 処理が割り込むか、割り込まないか、どうやったらわかるの? const fs = require("fs"); async function readFile1(){ return (await fs.promises.readFile('./test.txt')).toString(); } async function readFile2(){ return fs.readFileSync('./test.txt').toString(); } 「よく考えればわかります。処理がイベントキューに積まれることを意識するべきだ!」 とか言っていると見落としがちです。わかりにくいんです。 この例では、readFile1は割り込みますが、readFile2は割り込みません。 readFile1ではawaitを書かないでPromiseのままreturnすることもできるので 中身でawaitを使っているかどうか、は判断として使えません。 fs.promises.readFileはわかりやすい名前ですが、関数名で見分けがつくとも限りません。 async/awaitで説明しましたが、コールバックでも似たような状況 (Concurrent safeではない)が作れます。 質問の例に戻ると 通信Aを処理している最中に、通信Bが届いたとき、何が起こりますか? 通信A = ファイルの書き込み 通信B = ファイルの送信 という処理をする場合 (そもそも仕様が悪いというのはスルーするとして) ファイルが存在しなくて送れない、ファイルが古いものを送ってしまう ということが起こってしまうということです。 関数が割り込みされるかどうか(他の処理が)、 つまりシングルスレッドでも、マルチスレッド程ではないとしても クリティカルセクション、排他を意識したコーディングが必要になるということです。 どうすればいいの? いろんなやり方があります。 通信Aでflag=true(完了時にflag=false)して、通信Bをflag===trueで失敗にする 通信Aでflag=true(完了時にflag=false)して、通信Bでflag===falseになるまで待つ 通信A、Bをキューイングして、キューから処理を実行する async-lockを使う 3について想像が難しい場合は、マルチスレッドパターンですが、Active Object、Proactor、Reactor、Worker threadとか調べるといい説明がでてくるかもしれません。 4のasync-lockはスター数253のライブラリで、Lockという名前がついていますがキューのような動作をしていて、lock.acquireはpromiseを返すので、async/awaitな実装と一緒に使いやすいです。 async-await const AsyncLock = require('async-lock'); const lock = new AsyncLock(); async function sleep(ms) { return lock.acquire('mylock', async ()=>{ await new Promise(resolve => setTimeout(()=>{ console.log('sleep resolved'); resolve(); }, ms)); }); } async function callback(){ return lock.acquire('mylock', async()=>{ console.log('callback'); }); } async function exec(){ console.log('exec'); setTimeout(callback,1); const startTime = Date.now(); console.log('sleep started.'); await sleep(3000); console.log('sleep finished.', Date.now() - startTime, 'ms'); } exec(); % node sleep2.js exec sleep started. sleep resolved callback sleep finished. 3001 ms 'callback'はおおよそ3秒後に表示されます。 sleep関数のlockを抜けた直後にcallback関数のlock内処理が実行されます。 sleep finishedより先にcallbackが表示されることに注意して下さい。 lock.acquireは、他のlock.acquireと同時に実行されないことが保証できる lock.acquireを呼んだ順に実行される 注意すること return lock.acquire('mylock', async()=>{ return lock.acquire('mylock', async()=>{ console.log("hi"); }); }); このように入れ子になると、デッドロックのように止まって何も実行されなくなります。 関数を跨いで入れ子になる場合でも同様なので注意が必要です。 async-lockにはreentrantなlockも使えますが、個人的には特殊なケース以外では使わないように 使い方で注意したほうがいいでしょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DBDシリアルコード取得BOT

内容 discord botを使用してDBD(Dead By Daylight)のシリアルコードを神ゲー攻略から取得する 使い方 成果物のbot追加リンク サーバにBOTを招待して放置するだけ 開発環境 windows: 10 EXE Visual Studio Code: 1.62.3 エディタ Node.js: 16.13.0 パッケージ discord.js: 13.3.1 log4js: 6.3.0 ログ出力 node-cron: 3.0.0 定期実行 puppeteer: 11.0.0 データ取得 sqlite3: 5.0.2 ファイルツリー appディレクトリ下 appディレクトリ直下にnode_modulesもあるがツリーが見づらいため写してない パッケージインストール appディレクトリで下記コマンド実行 npm install puppeteer sqlite3 log4js node-cron discord.js ソース index index.js const {Client, Intents} = require('discord.js'); const client = new Client({intents: Object.keys(Intents.FLAGS)}); const {token, admin} = require('./Config/config.json'); const Log4js = require('log4js'); Log4js.configure('./Config/log-config.json'); const logger = Log4js.getLogger('system'); const common = require('./Config/common.json'); const name = 'Index'; const DataProcess = require('./Service/DataProcessService.js'); const dataprocess = new DataProcess(logger); client.on('ready', () => { logger.info(`login: user[${client.user.tag}]`); const cron = require('node-cron'); // 3時間ごとに実行 cron.schedule(common.scraping.interval, async() => { try{ logger.info(`call: ${name} ready`); const TimeProcess = require('./Service/TimeProcessService.js'); const timeprocess = new TimeProcess(logger); const lastTime = await timeprocess.getLastTime(); // 最終実行日時から1分が経過している場合 if (timeprocess.isTimehasPassend(lastTime.lastexetime)) { // 実行日時を記録 timeprocess.setLastExeTime(); const Scraping = require('./Service/ScrapingService.js'); const scraping = new Scraping(logger); // データ取得 const data = await scraping.getCode(lastTime.lastupdtime); // 記録と取得した更新日時が異なる場合 if (data.lastUpdTime) { // データ加工して送る timeprocess.setLastUpdTime(data.lastUpdTime); broadcastMsg(dataprocess.convertMsg(data.codeData)); } else { logger.info(`msg: ${name} 前回の取得情報と同じです`); } } else { logger.info(`msg: ${name} 時間を空けて実行してください`); } } catch(e) { logger.error(e); await broadcastMsg('問題が発生したため強制終了します'); logger.info(`logout: user[${client.user.tag}]`); process.exit(1); } }); }); client.on('message', async msg => { if (msg.author.id === admin) { // adminが送ったメッセージのみ記録 logger.info(`call: ${name} message[${msg}]`); if (msg.content.substr(0, 6) === '!noti ') { broadcastMsg(msg.content.substr(6)); }else if (msg.content === '!scnt') { msg.channel.send(`現在BOTが参加しているサーバ件数は[${client.guilds.cache.size}]です`); }else if (msg.content === '!gcid') { msg.channel.send(msg.channel.id); } } // オウム返し // if (msg.author !== client.user) { // msg.channel.send(msg.content); // } }); // 招待されたとき client.on('guildCreate', async guild => { try { const msgData = await dataprocess.getCodeMsg(); if (msgData.msg) { client.channels.cache.get(guild.systemChannelId).send(msgData.msg); } } catch(e) { logger.error(`call: guildCreate[${guild}]`); } }); client.login(token); function broadcastMsg(msg) { // BOTがいるテキストチャンネル[一般]にメッセージを送る const textChannels = client.channels.cache.filter(channel => { return channel.type == common.discord.sendChannelType && channel.name == common.discord.sendChannelName; }); Array.from(textChannels.keys()).forEach(channelId => { client.channels.cache.get(channelId).send(msg); }); } Service/ Service/ScrapingService.js class Scraping { constructor(logger) { this.logger = logger; this.common = require('../Config/common.json').scraping; } async getCode(lastTime) { this.logger.info(`call: ${this.constructor.name} getCode lastTime[${lastTime}]`); const url = this.common.targetUrl; const puppeteer = require('puppeteer'); const {options} = require('../Config/puppeteer.json'); const browser = await puppeteer.launch(options); const page = await browser.newPage(); // 対象のURLの情報のみ取得する await page.setRequestInterception(true); page.on('request', request => { if (url === request.url()) { request.continue(); } else { request.abort(); } }); const data = await this.getCodeFromWeb(page, url, lastTime); browser.close(); return data; } async getCodeFromWeb(page, url, lastTime){ this.logger.info(`call: ${this.constructor.name} getCodeFromWeb url[${url}] lastTime[${lastTime}]`); await page.goto(url); // データ取得 return await page.evaluate((common, lastTime) => { let data = { codeData : [] , lastUpdTime : "" }; let dataList = []; const updTime = document.querySelector('time').innerText; if (updTime === lastTime && common.timeFlg === 0) { // 記録と取得の更新日時が同じ場合 return data; } // 取得したいデータにidなどがなかったため長い記述 const storeCodeList = document.querySelector(common.targetTag).nextElementSibling.nextElementSibling.querySelectorAll('tr'); storeCodeList.forEach((elm, idx) => { let row = []; if (idx === 0) { elm.querySelectorAll('th').forEach(head => { row.push(head.innerText); }); dataList.push(row); row = []; } else { elm.querySelectorAll('td').forEach((body, itmIdx) => { if (itmIdx === 2) { row.push(body.querySelector('input').value); dataList.push(row); row = []; } else { row.push(body.innerText.replace(/\r?\n/g,"")); } }); } }); if (dataList.length === 0) { // データ0件の場合 throw new Error('Could not get the data'); } data.codeData = dataList; data.lastUpdTime = updTime; return data; // evaluteの引数 }, this.common, lastTime); } } module.exports = Scraping; Service/DataProcessService.js class DataProcess { constructor(logger) { this.logger = logger; this.common = require('../Config/common.json').dataProcess; this.Repository = require('../Repository/Repository.js'); this.repository = new this.Repository(logger); } convertMsg(data) { // データをメッセージ用にやや整える // コンソールで揃っていてもdiscordだとずれる this.logger.info(`call: ${this.constructor.name} convertMsg data[${data}]`); let msg = ""; const separater = " "; const newLine = '\n'; data.forEach((row, rowCnt) => { row.forEach((col, colCnt) => { if (rowCnt === 0) { msg += col + separater.repeat(40); } else { if (colCnt === 2) { msg += col; } else { msg += col + separater.repeat(((colCnt === 0 ? this.common.maxLimitLen : this.common.maxContentsLen) - this.getStrDataLen(col)) * 2); } msg += separater.repeat(6); } }); msg += newLine; }); this.setCodeMsg(msg); return msg; } getStrDataLen(str) { // 全角半角判定 let len = 0; [].forEach.call(str, txt => { (txt.match(/[ -~]/)) ? len += 1 : len += 2; }); return len; } setCodeMsg(msg) { // 作成したメッセージを記録 this.logger.info(`call: ${this.constructor.name} setCodeMsg msg[${msg}]`); this.repository.setCodeMsg(msg); } async getCodeMsg() { this.logger.info(`call: ${this.constructor.name} getCodeMsg`); return await this.repository.getCodeMsg(); } } module.exports = DataProcess; Service/TimeProcessService.js class TimeProcess { constructor(logger) { this.logger = logger; this.common = require('../Config/common.json').scraping; this.Repository = require('../Repository/Repository.js'); this.repository = new this.Repository(logger); } async getLastTime() { this.logger.info(`call: ${this.constructor.name} getLastTime`); return await this.repository.getLastTime(); } isTimehasPassend(time) { this.logger.info(`call: ${this.constructor.name} isTimehasPassend time[${time}]`); const now = new Date(); const lastTime = new Date(time); // 1分経過したら lastTime.setMinutes(lastTime.getMinutes() + this.common.progressMin); return now.getTime() > lastTime.getTime() ? true : false; } setLastExeTime() { this.logger.info(`call: ${this.constructor.name} setLastExeTime`); this.repository.setLastExeTime(this.convertToDate(new Date())); } setLastUpdTime(time) { this.logger.info(`call: ${this.constructor.name} setLastUpdTime time[${time}]`); this.repository.setLastUpdTime(time); } convertToDate(time) { this.logger.info(`call: ${this.constructor.name} convertToDate time[${time}]`); let datetime = ""; const timecut1 = '-'; const timecut2 = ':'; const space = ' '; datetime += time.getFullYear() + timecut1; datetime += time.getMonth() + 1 + timecut1; datetime += time.getDate() + space; datetime += time.getHours() + timecut2; datetime += time.getMinutes() + timecut2; datetime += time.getSeconds() + timecut2; datetime += time.getMilliseconds(); // console.log(datetime); return datetime; } now() { this.logger.info(`call: ${this.constructor.name} now`); return this.convertToDate(new Date()); } } module.exports = TimeProcess; Repository/ Repository/Repository.js class Repository { constructor(logger) { this.logger = logger; this.sqlite3 = require('sqlite3'); this.db = new this.sqlite3.Database('./dbd.sqlite'); // パスの場所はindexと同じらしい } getLastTime() { this.logger.info(`call: ${this.constructor.name} getLastTime`); return new Promise(resolve => { this.db.each('SELECT * FROM timeinfo', (err, row) => resolve(row)); }); } setLastExeTime(time) { this.logger.info(`call: ${this.constructor.name} setLastExeTime time[${time}]`); this.db.run('UPDATE timeinfo SET lastexetime = ?', time); } setLastUpdTime(time) { this.logger.info(`call: ${this.constructor.name} setLastUpdTime time[${time}]`); this.db.run('UPDATE timeinfo SET lastupdtime = ?', time); } // メッセージをそのまま記録 setCodeMsg(msg) { this.logger.info(`call: ${this.constructor.name} setCodeMsg msg[${msg}]`); this.db.run('UPDATE codemsg SET msg = ?', msg); } getCodeMsg() { this.logger.info(`call: ${this.constructor.name} getCodeMsg`); return new Promise(resolve => { this.db.each('SELECT msg FROM codemsg', (err, row) => resolve(row)); }); } } module.exports = Repository; Config/ Config/common.json { "scraping" : { "progressMin" : 1, 1分以内に2回以上のリクエストを無効 "targetUrl" : "https://kamigame.jp/dbd/page/116439381060878343.html", "targetTag" :"#引き換えコードの最新情報", "interval" : "0 0 */3 * * *", 指定時刻になると実行 3hおき 0, 3, 6, 9, 12...時 "timeFlg" : 0 0: 最終更新日時を確認, 他: 最終更新日時を無視 }, "discord" : { 一斉送信の対象 "sendChannelType" : "GUILD_TEXT", "sendChannelName" : "一般" }, "dataProcess" : { "maxLimitLen" : 6, "maxContentsLen" : 35 } } Config/config.json { "token" : "トークン" "admin" : "自分のdiscrod id" } Config/puppeteer.json { "args": [ puppeteer高速化 "--disable-gpu", "--disable-dev-shm-usage", "--disable-setuid-sandbox", "--no-first-run", "--no-sandbox", "--no-zygote", "--single-process" ] } Config/log-config.json ログの設定 { "appenders": { "console": { "type": "console" }, "system": { "type": "dateFile", "filename": "log/system.log", "pattern": "-yyyy-MM-dd" } }, "categories": { "default": { "appenders": [ "console", "system" ], "level": "all" } } } 最終実行日時の確認ですがnode-cronで3hおきに1回取得してくるので、1分以内に2回以上リクエストすることはないです。 最初はメッセージに反応してデータを取得するようにしていたのですが、メッセージを送るのが面倒だったため定期実行に変えました。node-cronの設定を間違えたとき用でそのまま置いてあります。 参考サイト js スクレイピング https://qiita.com/ledsun/items/0965a60f9bdff04f2fa0 pupptter高速化 https://qiita.com/markey/items/ebf2b48626b6ac61ee89 puppetter 引数 https://qiita.com/horikeso/items/f87d3e703828aa13e2ff id無しエレメント取得 https://gray-code.com/javascript/get-child-element-and-paranet-element-and-previous-element-and-next-element-of-specific-html-element/ 非同期処理 https://qiita.com/kerupani129/items/cf4048a7d4e3aad75881 sqlite https://qiita.com/zaburo/items/a155cbc02832b501a8dd ログファイル出力 https://webbibouroku.com/Blog/Article/log4js node 定期実行 https://qiita.com/n0bisuke/items/66abf6ca1c12f495aa04
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む