シンプルな機能の裏に掛かる労働時間
Posted on by Angus Cheng
このウェブサイトを2021年4月から開始し、ユーザーはパスワード保護されいるPDFファイルをアップロードをアップロードしたい事は分かっていた。これらのPDFのサポートを行う事は面倒だった為、手を付けなかった。ただ、最近、ユーザーからパスワード保護が掛かったPDFを対応してくれないかと依頼があった為、やる事にした。
これはシンプルな機能で結構うまくいきます。昨日はほとんど丸一日作業をしていて、今朝午前2:32まで仕事をしていました。この数日収集した少量のデータによると、約5%のユーザーがパスワード付のPDFをアップロードしている事が分かった。今までは画面にエラーメッセージが出る前にアップロードする事自体却下していた。これらのほとんどのユーザーは、ウェブサイト自体上手くいかないと想定し、どこかへ消えてしまった。
この機能を実現するに辺り掛かった時間には驚き、不満を感じたので、あなたが私に感謝出来る様、私があなたの為に行った内容を共有します。1
土曜日 午後11:46 – ファイルマッピングテーブルにフィールドを追加する。
ファイルマッピング
テーブルはユーザーがアップロードした各PDFの情報を保存する。2つのフィールドパスワード
と パスワード必須
を追加した。 このアイディアはパスワードやその他書類のメタデータをデータベースへ保存する事だ。
日曜日 午前11:10 – APIが保護されたPDFを受け取った場合
APIを変えて、ファイルマッピング
を作成し、パスワード必須
で記録し、PDF種類は不明
とした。 以前まではPDFはTEXT_BASED
やIMAGE_BASED
で対応する事ができた。パスワード無しではファイルを読むことは出来ないので、TEXT_BASED か IMAGE_BASEDか分析し決定する事も出来ない。
これをする理由はテキストかイメージのPDFによってコードが別になるからだ。
日曜日 午前1:14 - SQLを書き、ファイルマッピングレコードをアップロード
UPDATE file_mapping SET password = ? WHERE uuid = ?;
日曜日 午後 2:55 – SQLを変更し、APIの作成をし書類のパスワードを設定
UPDATE file_mapping SET password = ? pdf_type = ? page_count = ? WHERE uuid = ?;
APIのコーディングを行い、ユーザーがパスワード設定をする事に成功した後、フィールドマッピングのフィールドを変えたいと思い始めた。書類を読んだ後、PDF種類を分類し、何ページあるかカウントするが可能となる。
val fileMappings = repository.getFileMappings(body.passwords.map { it.uuid })
val updates = mutableMapOf<String, UpdateFileMapping>()
val results = fileMappings.map { mapping ->
mapping.validateOwnership(userId, ipAddress)
val password = body.passwords.first { it.uuid == mapping.uuid}.password
val file = File(mapping.filename)
val result = uploadAction.analysePdf(file, password, userId, ipAddress, mapping.uuid, mapping.originalFilename)
if (result.state != UploadResponse.State.REQUIRES_PASSWORD) {
updates[mapping.uuid] = UpdateFileMapping(password, result.pdfType.toString(), result.numberOfPages)
}
result
}
// Set the passwords
repository.updateFileMappings(updates)
call.respondText(contentType = ContentType.Application.Json) {
stringify(results)
}
APIは書類識別子やパスワードを受け入れています。その後、提供されたパスワードで書類を開く事を試みます。データベースに正しいパスワードを書くのです。そして提供されたパスワードでどの書類を開く事に成功できたかの返答をします。
日曜日 午後3:21 - 書類開示コードを変更し、可能な際にパスワードを使用できる様にした。
val document = if (password == null) Loader.loadPDF(statement) else Loader.loadPDF(statement, password)
パスワードを持っているのであれば、使ってください。これは実際にはとてもシンプルです。Loader.loadPDFにてパスワードの無効値を喜んで受け入れてくれます。
日曜日 午後11:57 - 全てのLoader.loadPDFにパスワードを提供してもらうのは結
構簡単ですが、保護されたPDFの全ての変換路を確認する為の様々なテストを作成しました。時間に大きなギャップがあるのは、親父の家に行って、昼寝をし、夕飯を食べてから香港フィルムアワードを見ていたからです。
月曜日 午前12:30 – DEV内のフォルダー消去バグの修正
定期的にPDFなどのユーザーデータのクリーンアップの仕事を行う事があり、画像や光学式文字認識の結果などを提供している。
for (directory in directories) {
for (file in directory.walk()) {
// delete file
}
}
ディレクトリが空の場合、上記のコードはディレクトリから消去される。プロダクションサーバーにはこれが発生しない理由は人々は常にアプリを使用している為、ディレクトリが空になる事が無いからだ。ただし、開発サーバーでは発生します! この問題に関しては下記のコードでバグを修正した。
fun start(): AccountingProApp {
tempFileDirectories.forEach {
if (!it.exists()) {
it.mkdirs()
}
}
startServerWithRetries()
cleanup.scheduleExecution()
imagePdfWorker.scheduleExecution()
return this
}
アプリ立ち上げ時にディレクトリが存在しない場合、コードが作成をします。ただ、この投稿を書いてる際に、この修正法はあまり良くない事に気づきました。空のフォルダーは削除する事はできますが、取り戻す為にはサーバーをバウンスする必要があります。開発サーバーは毎回コードを再移動する度に頻繁にバウンスします。なので、余分な手間を省くためにフォルダーを削除しない解決法を思いつきました。
月曜 午前2:24 - UIが書類のパスワード設定を認める
<Input onChange={onChange} value={value} placeholder="Password"/>
<Button
onClick={onClick}
css={{ marginLeft: '0.25em' }}
size="compact"
emphasis="primary">
Submit
</Button>
保護されたPDFをアップロードする際にインプットエレメントを提供します。提出ボタンを押す際にAPIを押します。ブラウザにAPIの回答を一致させます。私はそんなにフロントエンドコードを書くのは上手ではないので、とても面倒でした。
見た目も良くないですし、インプットボックスも大きすぎます。CSSを書くのは好きではないですし、機能的には大丈夫そうなので、ここでストップしました。
月曜日 午前2:32 - APIのロジック修正
PDFベースのテキストは描写や文字認識されている事に気づいた。ただ正しくはない。なので、バグ修正を行った。コードをプロダクションへ入れ込んだ後、就寝した。
月曜日 午前11:13 - UI最後の仕上げ
私のオフィスへ行き、Grafanaダッシュボードを確認すると、誰かがパスワード機能に成功したとの事だった。これは嬉しい!ローディングインディケーターを追加し、インプットエレメントを小さくしました。
<Input
size="compact"
onChange={onChange}
value={value}
placeholder="Password"
disabled={disabled}
/>
<Button
onClick={onClick}
css={{ marginLeft: '0.25em' }}
size="compact"
emphasis="primary"
disabled={disabled}
decoratorLeft={decoratorLeft}
>
Submit
</Button>
Monday 11:38 AM - Fixed another API bug
// Before
val state = if (it.uuid in inProgressUuids) UploadResponse.State.PROCESSING else UploadResponse.State.READY
// After
val state = when {
it.uuid in inProgressUuids -> UploadResponse.State.PROCESSING
it.pdfType == PDFType.UNKNOWN -> UploadResponse.State.REQUIRES_PASSWORD
else -> UploadResponse.State.READY
}
コードは2つの選択肢、READYかPROCESSINGしかない。この新しい機能にはREQUIRES_PASSWORDが加わった。
結論
もう一度こちらのデモをご確認下さい。こんなにシンプルな機能に数日掛かり、香港映フィルムアワードも観てやっと完了したとは信じられませんね。
この仕事をやって良かったか?それは時間が経てば分かるでしょう。この投稿を2032年に見ている方がいましたらぜひ連絡くだされば、この機能でどの位お金を稼ぐ事が出来たか分析します。“保護ファイルをアップロードしたユーザーからの収益を計算します”。2033年になったら、この分析を行います。
-
Your money ↩︎