2022/02/18
[Rust][actix-web][shift-jis]csvファイルダウンロード
- actix-webを使って、csvファイルを作ってダウンロードするエンドポイントを作ったので、それの備忘録です
- rustでのcsvファイルの作り方や、shift-jisのエンコーディングなどの参考にもなります。
- 末尾にclient側の実装も添付しています。
依存関係
cargo.toml
以下が今回特に必要になるライブラリです。
※ actix-webはbeta版を使っているので注意してください。
actix-web = "4.0.0-beta.10"
actix-files = "0.6.0-beta.16"
actix-cors = "0.6.0-beta.10"
csv = "1.1.6"
encoding_rs = "0.8.30"
実装
API handler
かなりデフォルメして記載します。
windowsユーザーのために、shift jisにエンコードしています。
use crate::Error;
use actix_files::NamedFile;
use actix_web::http::header::{ContentDisposition, HeaderValue};
use actix_web::{web, HttpResponse};
use chrono::Utc;
use std::path::PathBuf;
use csv::{ByteRecord, WriterBuilder};
pub async fn handler(request: web::HttpRequest) -> Result<HttpResponse, Error> {
let filename = "download_filename.tsv";
let now = Utc::now().timestamp_millis();
let tmp_filename = format!("{}_{}.tsv", &filename, now);
let tmp_dir = PathBuf::from("/tmp");
let filepath = tmp_dir.join(tmp_filename.as_str());
let mut wtr = WriterBuilder::new()
.from_path(filepath.as_path())?;
let encoding = encoding_rs::SHIFT_JIS;
// もしUTF8だったら以下(私は開発中はconfig値などでここを動的に切り替えれるようにしています。)
// let encoding = encoding_rs::UTF_8;
// header行の出力
let header = vec![
"会員ID",
"姓",
"名",
"メールアドレス",
];
let header: Vec<Vec<u8>> = header
.iter()
.map(|s| encoding.encode(s).0.as_ref().to_vec())
.collect();
let header_record = ByteRecord::from(header);
wtr.write_byte_record(&header_record)?;
// 行の出力(本来ならdbから1000件毎とかにデータを取得して、書き込みます。
let row = vec![
"1",
"佐藤",
"太郎",
"[email protected]",
];
let row: Vec<Vec<u8>> = row
.iter()
.map(|s| encoding.encode(s).0.as_ref().to_vec())
.collect();
let row_record = ByteRecord::from(row);
wtr.write_byte_record(&row_record)?;
// 書き込み実行
let _ok = wtr.flush()?;
let mime_type = "text/csv".parse().unwrap();
let content_disposition = format!("attachment; filename=\"{}\"", tmp_filename.as_str());
let content_disposition = HeaderValue::from_str(content_disposition.as_str()).unwrap();
let content_disposition = ContentDisposition::from_raw(&content_disposition).unwrap();
// actix_files::NamedFile を使ってresponseに変換します
let named_file = NamedFile::open(filepath)?
.set_content_type(mime_type)
.set_content_disposition(content_disposition);
Ok(named_file.into_response(&request))
}
CorsのTips
actix_cors::Cors
middlewareで、content-disposition
headerをexposeするように指定します。
これをすることで、content-disposition
headerの値をclient側のjavascriptで読み取れるようになります。
use actix_cors::Cors;
use actix_web::web::Data;
use actix_web::{http::header, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 中略
HttpServer::new(move || {
let mut cors = Cors::default();
for allowed_origin in app_config.allowed_origins.as_slice() {
cors = cors.allowed_origin(allowed_origin.as_str())
}
let cors = cors
.allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "PATCH"])
.allowed_headers(vec![
header::AUTHORIZATION,
header::CONTENT_TYPE,
header::ACCEPT,
])
.expose_headers(vec![header::CONTENT_DISPOSITION]) // ★ ここがポイント
.max_age(3600);
App::new()
.app_data(Data::new(psql_connection_pool.clone()))
// その他省略
.wrap(cors)
// その他省略
.configure(endpoint::routes)
})
.bind(app_config.bind_address.as_str())?
.run()
.await
}
Client側
ダウンロードボタンクリック時などに、以下のようにして上記のendpointを呼び出します。
const response = await axios.get('/your-download-endpoint-path', {
responseType: 'blob',
})
const blob = new Blob([response.data], {
type: 'text/csv',
})
const headers = response.headers['content-disposition'].split(';')
const target = headers.find((item) => item.includes('filename='))
const fileName = target.trim().replace(/"/g, '').split('=')[1]
if (window.navigator.msSaveOrOpenBlob) {
// for IE,Edge
window.navigator.msSaveOrOpenBlob(blob, fileName)
} else {
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
window.URL.revokeObjectURL(url)
link.parentNode.removeChild(link)
}
以上です。
関連する記事
Concordiumノードをローカルで動かしてみた
Concordiumの調査のために、ローカルでソースコードをビルドしてノードを動かしてみました
[Rust]axumとdragonflyを使ったWebsocket Chatのサンプル実装
redis互換のdragonflyをPUBSUBとして利用して、Websocket Chatアプリのサンプル実装を行いました。
[Rust]TiDBを使ったサンプルアプリケーションの実装
RustからTiDBを使ったアプリケーションの実装を行いました。
[Rust]Google Cloud Storageを利用する
GCSやNFSのファイルを扱えるpackageをRustで実装しました。