Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ pub trait BacklogApi {
fn get_space(&self) -> Result<Space> {
unimplemented!()
}
fn download_space_image(&self) -> Result<(Vec<u8>, String)> {
unimplemented!()
}
fn get_rate_limit(&self) -> Result<RateLimit> {
unimplemented!()
}
Expand Down Expand Up @@ -764,6 +767,10 @@ impl BacklogApi for BacklogClient {
self.get_space()
}

fn download_space_image(&self) -> Result<(Vec<u8>, String)> {
self.download_space_image()
}

fn get_rate_limit(&self) -> Result<RateLimit> {
self.get_rate_limit()
}
Expand Down
4 changes: 4 additions & 0 deletions src/api/space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ impl BacklogClient {
let value = self.get("/space")?;
deserialize(value)
}

pub fn download_space_image(&self) -> Result<(Vec<u8>, String)> {
self.download("/space/image")
}
}

#[cfg(test)]
Expand Down
141 changes: 141 additions & 0 deletions src/cmd/space/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use anstream::println;
use anyhow::{Context, Result};
use std::path::PathBuf;

use crate::api::{BacklogApi, BacklogClient};

pub struct SpaceImageArgs {
output: Option<PathBuf>,
}

impl SpaceImageArgs {
pub fn new(output: Option<PathBuf>) -> Self {
Self { output }
}
}

pub fn image(args: &SpaceImageArgs) -> Result<()> {
let client = BacklogClient::from_config()?;
image_with(args, &client)
}

pub fn image_with(args: &SpaceImageArgs, api: &dyn BacklogApi) -> Result<()> {
let (bytes, filename) = api.download_space_image()?;
let path = args
.output
.clone()
.unwrap_or_else(|| default_output_path(&filename));
std::fs::write(&path, &bytes).with_context(|| format!("Failed to write {}", path.display()))?;
println!("Saved: {} ({} bytes)", path.display(), bytes.len());
Ok(())
}

fn default_output_path(filename: &str) -> PathBuf {
let normalized = filename.trim();
let lower = normalized.to_ascii_lowercase();
let is_generic_attachment = lower == "attachment" || lower.starts_with("attachment.");

let effective = if normalized.is_empty() || is_generic_attachment {
"space_image"
} else {
normalized
};
let base = std::path::Path::new(effective)
.file_name()
.unwrap_or(std::ffi::OsStr::new("space_image"));
PathBuf::from(base)
}

#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
use tempfile::tempdir;

struct MockApi {
result: Option<(Vec<u8>, String)>,
}

impl crate::api::BacklogApi for MockApi {
fn download_space_image(&self) -> anyhow::Result<(Vec<u8>, String)> {
self.result
.clone()
.ok_or_else(|| anyhow!("download failed"))
}
}

fn args(output: Option<PathBuf>) -> SpaceImageArgs {
SpaceImageArgs::new(output)
}

#[test]
fn image_with_saves_file_to_specified_path() {
let dir = tempdir().unwrap();
let path = dir.path().join("out.png");
let api = MockApi {
result: Some((b"png-data".to_vec(), "space_image.png".to_string())),
};
assert!(image_with(&args(Some(path.clone())), &api).is_ok());
assert_eq!(std::fs::read(&path).unwrap(), b"png-data");
}

#[test]
fn image_with_saves_file_to_default_filename() {
let dir = tempdir().unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&dir).unwrap();

struct DirGuard(std::path::PathBuf);
impl Drop for DirGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.0);
}
}
let _guard = DirGuard(original_dir);

let api = MockApi {
result: Some((b"png-data".to_vec(), "space_image.png".to_string())),
};
assert!(image_with(&args(None), &api).is_ok());
assert_eq!(
std::fs::read(dir.path().join("space_image.png")).unwrap(),
b"png-data"
);
}

#[test]
fn image_with_propagates_api_error() {
let api = MockApi { result: None };
let err = image_with(&args(None), &api).unwrap_err();
assert!(err.to_string().contains("download failed"));
}

#[test]
fn default_output_path_uses_server_filename() {
assert_eq!(
default_output_path("space_image.png"),
PathBuf::from("space_image.png")
);
}

#[test]
fn default_output_path_falls_back_for_attachment() {
assert_eq!(
default_output_path("attachment"),
PathBuf::from("space_image")
);
}

#[test]
fn default_output_path_falls_back_for_attachment_with_extension() {
assert_eq!(
default_output_path("attachment.png"),
PathBuf::from("space_image")
);
}

#[test]
fn default_output_path_falls_back_for_empty() {
assert_eq!(default_output_path(""), PathBuf::from("space_image"));
}
}
2 changes: 2 additions & 0 deletions src/cmd/space/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod activities;
mod disk_usage;
mod image;
mod licence;
mod notification;
mod show;
mod update_notification;

pub use activities::{SpaceActivitiesArgs, activities};
pub use image::{SpaceImageArgs, image};

#[cfg(test)]
pub(crate) fn sample_notification() -> crate::api::space_notification::SpaceNotification {
Expand Down
13 changes: 11 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ use cmd::rate_limit::RateLimitArgs;
use cmd::resolution::ResolutionListArgs;
use cmd::shared_file::{SharedFileGetArgs, SharedFileListArgs};
use cmd::space::{
SpaceActivitiesArgs, SpaceDiskUsageArgs, SpaceLicenceArgs, SpaceNotificationArgs,
SpaceShowArgs, SpaceUpdateNotificationArgs,
SpaceActivitiesArgs, SpaceDiskUsageArgs, SpaceImageArgs, SpaceLicenceArgs,
SpaceNotificationArgs, SpaceShowArgs, SpaceUpdateNotificationArgs,
};
use cmd::star::{StarAddArgs, StarDeleteArgs};
use cmd::team::{
Expand Down Expand Up @@ -336,6 +336,12 @@ enum SpaceCommands {
#[arg(long)]
json: bool,
},
/// Download the space icon image
Image {
/// Output file path (default: server-provided filename, or "space_image" if none)
#[arg(long, short = 'o')]
output: Option<std::path::PathBuf>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -3695,6 +3701,9 @@ fn run() -> Result<()> {
content,
json || sub_json,
)),
Some(SpaceCommands::Image { output }) => {
cmd::space::image(&SpaceImageArgs::new(output))
}
},
}
}
19 changes: 18 additions & 1 deletion website/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ Updated: 2024-07-01T00:00:00Z
Scheduled maintenance on 2024-07-01.
```

## `bl space image`

Download the space icon image.

The response is binary data. Use `--output` / `-o` to specify where the file is written. If omitted, the command saves the file in the current directory using the filename returned by the server (or `space_image` if none is provided).

```bash
bl space image
bl space image --output my_icon.png
```

Example output:

```text
Saved: space_image.png (1234 bytes)
```

## `bl project list`

List all projects you have access to.
Expand Down Expand Up @@ -2182,7 +2199,7 @@ The table below maps Backlog API v2 endpoints to `bl` commands.
| `bl space notification` | `GET /api/v2/space/notification` | ✅ Implemented |
| `bl space licence` | `GET /api/v2/space/licence` | ✅ Implemented |
| `bl space update-notification` | `PUT /api/v2/space/notification` | ✅ Implemented |
| | `GET /api/v2/space/image` | Planned |
| `bl space image` | `GET /api/v2/space/image` | ✅ Implemented |
| — | `POST /api/v2/space/attachment` | Planned |

### Projects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ Updated: 2024-07-01T00:00:00Z
メンテナンスのお知らせ
```

## `bl space image`

スペースのアイコン画像をダウンロードします。

レスポンスはバイナリデータです。`--output` / `-o` で保存先を指定してください。省略した場合は、サーバーが返すファイル名(取得できない場合は `space_image`)を使ってカレントディレクトリに保存します。

```bash
bl space image
bl space image --output my_icon.png
```

出力例:

```text
Saved: space_image.png (1234 bytes)
```

## `bl project list`

アクセス可能なプロジェクトを一覧表示します。
Expand Down Expand Up @@ -2184,7 +2201,7 @@ Backlog API v2 エンドポイントと `bl` コマンドの対応表です。
| `bl space notification` | `GET /api/v2/space/notification` | ✅ 実装済み |
| `bl space licence` | `GET /api/v2/space/licence` | ✅ 実装済み |
| `bl space update-notification` | `PUT /api/v2/space/notification` | ✅ 実装済み |
| | `GET /api/v2/space/image` | 計画中 |
| `bl space image` | `GET /api/v2/space/image` | ✅ 実装済み |
| — | `POST /api/v2/space/attachment` | 計画中 |

### Projects
Expand Down