4 use anyhow
::{bail, Error}
;
10 list_subdirs_api_method
,
25 drive
::check_drive_exists
,
33 MEDIA_POOL_NAME_SCHEMA
,
41 LinuxDriveAndMediaStatus
,
43 tape
::restore
::restore_media
,
52 linux_tape_device_list
,
62 open_linux_tape_device
,
64 required_media_changer
,
67 changer
::update_changer_online_status
,
75 schema
: DRIVE_NAME_SCHEMA
,
78 schema
: MEDIA_LABEL_SCHEMA
,
83 /// Load media with specified label
85 /// Issue a media load request to the associated changer device.
86 pub async
fn load_media(drive
: String
, label_text
: String
) -> Result
<(), Error
> {
88 let (config
, _digest
) = config
::drive
::config()?
;
90 tokio
::task
::spawn_blocking(move || {
91 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
92 changer
.load_media(&label_text
)
100 schema
: DRIVE_NAME_SCHEMA
,
103 description
: "Source slot number.",
109 /// Load media from the specified slot
111 /// Issue a media load request to the associated changer device.
112 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
114 let (config
, _digest
) = config
::drive
::config()?
;
116 tokio
::task
::spawn_blocking(move || {
117 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
118 changer
.load_media_from_slot(source_slot
)
126 schema
: DRIVE_NAME_SCHEMA
,
129 schema
: MEDIA_LABEL_SCHEMA
,
134 description
: "The import-export slot number the media was transfered to.",
139 /// Export media with specified label
140 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
142 let (config
, _digest
) = config
::drive
::config()?
;
144 tokio
::task
::spawn_blocking(move || {
145 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
146 match changer
.export_media(&label_text
)?
{
147 Some(slot
) => Ok(slot
),
148 None
=> bail
!("media '{}' is not online (via changer '{}')", label_text
, changer_name
),
157 schema
: DRIVE_NAME_SCHEMA
,
160 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
167 /// Unload media via changer
170 target_slot
: Option
<u64>,
172 ) -> Result
<(), Error
> {
174 let (config
, _digest
) = config
::drive
::config()?
;
176 tokio
::task
::spawn_blocking(move || {
177 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
178 changer
.unload_media(target_slot
)
187 description
: "The list of autodetected tape drives.",
190 type: TapeDeviceInfo
,
195 pub fn scan_drives(_param
: Value
) -> Result
<Vec
<TapeDeviceInfo
>, Error
> {
197 let list
= linux_tape_device_list();
206 schema
: DRIVE_NAME_SCHEMA
,
209 description
: "Use fast erase.",
224 rpcenv
: &mut dyn RpcEnvironment
,
225 ) -> Result
<Value
, Error
> {
227 let (config
, _digest
) = config
::drive
::config()?
;
229 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
231 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
233 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
235 let upid_str
= WorkerTask
::new_thread(
241 let mut drive
= open_drive(&config
, &drive
)?
;
242 drive
.erase_media(fast
.unwrap_or(true))?
;
254 schema
: DRIVE_NAME_SCHEMA
,
265 rpcenv
: &mut dyn RpcEnvironment
,
266 ) -> Result
<Value
, Error
> {
268 let (config
, _digest
) = config
::drive
::config()?
;
270 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
272 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
274 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
276 let upid_str
= WorkerTask
::new_thread(
282 let mut drive
= open_drive(&config
, &drive
)?
;
295 schema
: DRIVE_NAME_SCHEMA
,
303 /// Eject/Unload drive media
306 rpcenv
: &mut dyn RpcEnvironment
,
307 ) -> Result
<Value
, Error
> {
309 let (config
, _digest
) = config
::drive
::config()?
;
311 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
313 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
315 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
317 let upid_str
= WorkerTask
::new_thread(
323 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
324 changer
.unload_media(None
)?
;
326 let mut drive
= open_drive(&config
, &drive
)?
;
327 drive
.eject_media()?
;
339 schema
: DRIVE_NAME_SCHEMA
,
342 schema
: MEDIA_LABEL_SCHEMA
,
345 schema
: MEDIA_POOL_NAME_SCHEMA
,
356 /// Write a new media label to the media in 'drive'. The media is
357 /// assigned to the specified 'pool', or else to the free media pool.
359 /// Note: The media need to be empty (you may want to erase it first).
362 pool
: Option
<String
>,
364 rpcenv
: &mut dyn RpcEnvironment
,
365 ) -> Result
<Value
, Error
> {
367 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
369 if let Some(ref pool
) = pool
{
370 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
372 if pool_config
.sections
.get(pool
).is_none() {
373 bail
!("no such pool ('{}')", pool
);
377 let (config
, _digest
) = config
::drive
::config()?
;
379 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
381 let upid_str
= WorkerTask
::new_thread(
388 let mut drive
= open_drive(&config
, &drive
)?
;
392 match drive
.read_next_file() {
393 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
394 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
396 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
397 /* assume tape is empty */
399 bail
!("media read error - {}", err
);
404 let ctime
= proxmox
::tools
::time
::epoch_i64();
405 let label
= MediaLabel
{
406 label_text
: label_text
.to_string(),
407 uuid
: Uuid
::generate(),
411 write_media_label(worker
, &mut drive
, label
, pool
)
418 fn write_media_label(
419 worker
: Arc
<WorkerTask
>,
420 drive
: &mut Box
<dyn TapeDriver
>,
422 pool
: Option
<String
>,
423 ) -> Result
<(), Error
> {
425 drive
.label_tape(&label
)?
;
427 let mut media_set_label
= None
;
429 if let Some(ref pool
) = pool
{
430 // assign media to pool by writing special media set label
431 worker
.log(format
!("Label media '{}' for pool '{}'", label
.label_text
, pool
));
432 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
, None
);
434 drive
.write_media_set_label(&set
, None
)?
;
435 media_set_label
= Some(set
);
437 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.label_text
));
440 let media_id
= MediaId { label, media_set_label }
;
442 let status_path
= Path
::new(TAPE_STATUS_DIR
);
444 // Create the media catalog
445 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
447 let mut inventory
= Inventory
::load(status_path
)?
;
448 inventory
.store(media_id
.clone(), false)?
;
452 match drive
.read_label() {
453 Ok((Some(info
), _
)) => {
454 if info
.label
.uuid
!= media_id
.label
.uuid
{
455 bail
!("verify label failed - got wrong label uuid");
457 if let Some(ref pool
) = pool
{
458 match info
.media_set_label
{
460 if set
.uuid
!= [0u8; 16].into() {
461 bail
!("verify media set label failed - got wrong set uuid");
463 if &set
.pool
!= pool
{
464 bail
!("verify media set label failed - got wrong pool");
468 bail
!("verify media set label failed (missing set label)");
473 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
474 Err(err
) => bail
!("verify label failed - {}", err
),
487 schema
: DRIVE_NAME_SCHEMA
,
490 description
: "Encryption key password.",
495 /// Try to restore a tape encryption key
496 pub async
fn restore_key(
499 ) -> Result
<(), Error
> {
501 let (config
, _digest
) = config
::drive
::config()?
;
503 tokio
::task
::spawn_blocking(move || {
504 let mut drive
= open_drive(&config
, &drive
)?
;
506 let (_media_id
, key_config
) = drive
.read_label()?
;
508 if let Some(key_config
) = key_config
{
509 let password_fn
= || { Ok(password.as_bytes().to_vec()) }
;
510 let (key
, ..) = key_config
.decrypt(&password_fn
)?
;
511 config
::tape_encryption_keys
::insert_key(key
, key_config
, true)?
;
513 bail
!("media does not contain any encryption key configuration");
524 schema
: DRIVE_NAME_SCHEMA
,
527 description
: "Inventorize media",
536 /// Read media label (optionally inventorize media)
537 pub async
fn read_label(
539 inventorize
: Option
<bool
>,
540 ) -> Result
<MediaIdFlat
, Error
> {
542 let (config
, _digest
) = config
::drive
::config()?
;
544 tokio
::task
::spawn_blocking(move || {
545 let mut drive
= open_drive(&config
, &drive
)?
;
547 let (media_id
, _key_config
) = drive
.read_label()?
;
549 let media_id
= match media_id
{
551 let mut flat
= MediaIdFlat
{
552 uuid
: media_id
.label
.uuid
.clone(),
553 label_text
: media_id
.label
.label_text
.clone(),
554 ctime
: media_id
.label
.ctime
,
555 media_set_ctime
: None
,
556 media_set_uuid
: None
,
557 encryption_key_fingerprint
: None
,
561 if let Some(ref set
) = media_id
.media_set_label
{
562 flat
.pool
= Some(set
.pool
.clone());
563 flat
.seq_nr
= Some(set
.seq_nr
);
564 flat
.media_set_uuid
= Some(set
.uuid
.clone());
565 flat
.media_set_ctime
= Some(set
.ctime
);
566 flat
.encryption_key_fingerprint
= set
567 .encryption_key_fingerprint
569 .map(|fp
| crate::tools
::format
::as_fingerprint(fp
.bytes()));
571 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
572 .map(|fp
| (fp
, set
.uuid
.clone()));
574 if let Err(err
) = drive
.set_encryption(encrypt_fingerprint
) {
575 // try, but ignore errors. just log to stderr
576 eprintln
!("uable to load encryption key: {}", err
);
580 if let Some(true) = inventorize
{
581 let state_path
= Path
::new(TAPE_STATUS_DIR
);
582 let mut inventory
= Inventory
::load(state_path
)?
;
583 inventory
.store(media_id
, false)?
;
589 bail
!("Media is empty (no label).");
601 schema
: DRIVE_NAME_SCHEMA
,
612 rpcenv
: &mut dyn RpcEnvironment
,
613 ) -> Result
<Value
, Error
> {
615 let (config
, _digest
) = config
::drive
::config()?
;
617 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
619 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
621 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
623 let upid_str
= WorkerTask
::new_thread(
630 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
632 worker
.log("Starting drive clean");
634 changer
.clean_drive()?
;
636 worker
.log("Drive cleaned sucessfully");
648 schema
: DRIVE_NAME_SCHEMA
,
653 description
: "The list of media labels with associated media Uuid (if any).",
660 /// List known media labels (Changer Inventory)
662 /// Note: Only useful for drives with associated changer device.
664 /// This method queries the changer to get a list of media labels.
666 /// Note: This updates the media online status.
667 pub async
fn inventory(
669 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
671 let (config
, _digest
) = config
::drive
::config()?
;
673 tokio
::task
::spawn_blocking(move || {
674 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
676 let label_text_list
= changer
.online_media_label_texts()?
;
678 let state_path
= Path
::new(TAPE_STATUS_DIR
);
680 let mut inventory
= Inventory
::load(state_path
)?
;
682 update_changer_online_status(
689 let mut list
= Vec
::new();
691 for label_text
in label_text_list
.iter() {
692 if label_text
.starts_with("CLN") {
693 // skip cleaning unit
697 let label_text
= label_text
.to_string();
699 if let Some(media_id
) = inventory
.find_media_by_label_text(&label_text
) {
700 list
.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) }
);
702 list
.push(LabelUuidMap { label_text, uuid: None }
);
714 schema
: DRIVE_NAME_SCHEMA
,
717 description
: "Load all tapes and try read labels (even if already inventoried)",
729 /// Note: Only useful for drives with associated changer device.
731 /// This method queries the changer to get a list of media labels. It
732 /// then loads any unknown media into the drive, reads the label, and
733 /// store the result to the media database.
735 /// Note: This updates the media online status.
736 pub fn update_inventory(
738 read_all_labels
: Option
<bool
>,
739 rpcenv
: &mut dyn RpcEnvironment
,
740 ) -> Result
<Value
, Error
> {
742 let (config
, _digest
) = config
::drive
::config()?
;
744 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
746 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
748 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
750 let upid_str
= WorkerTask
::new_thread(
757 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
759 let label_text_list
= changer
.online_media_label_texts()?
;
760 if label_text_list
.is_empty() {
761 worker
.log("changer device does not list any media labels".to_string());
764 let state_path
= Path
::new(TAPE_STATUS_DIR
);
766 let mut inventory
= Inventory
::load(state_path
)?
;
768 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
770 for label_text
in label_text_list
.iter() {
771 if label_text
.starts_with("CLN") {
772 worker
.log(format
!("skip cleaning unit '{}'", label_text
));
776 let label_text
= label_text
.to_string();
778 if !read_all_labels
.unwrap_or(false) && inventory
.find_media_by_label_text(&label_text
).is_some() {
779 worker
.log(format
!("media '{}' already inventoried", label_text
));
783 if let Err(err
) = changer
.load_media(&label_text
) {
784 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
788 let mut drive
= open_drive(&config
, &drive
)?
;
789 match drive
.read_label() {
791 worker
.warn(format
!("unable to read label form media '{}' - {}", label_text
, err
));
794 worker
.log(format
!("media '{}' is empty", label_text
));
796 Ok((Some(media_id
), _key_config
)) => {
797 if label_text
!= media_id
.label
.label_text
{
798 worker
.warn(format
!("label text missmatch ({} != {})", label_text
, media_id
.label
.label_text
));
801 worker
.log(format
!("inventorize media '{}' with uuid '{}'", label_text
, media_id
.label
.uuid
));
802 inventory
.store(media_id
, false)?
;
805 changer
.unload_media(None
)?
;
819 schema
: DRIVE_NAME_SCHEMA
,
822 schema
: MEDIA_POOL_NAME_SCHEMA
,
831 /// Label media with barcodes from changer device
832 pub fn barcode_label_media(
834 pool
: Option
<String
>,
835 rpcenv
: &mut dyn RpcEnvironment
,
836 ) -> Result
<Value
, Error
> {
838 if let Some(ref pool
) = pool
{
839 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
841 if pool_config
.sections
.get(pool
).is_none() {
842 bail
!("no such pool ('{}')", pool
);
846 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
848 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
850 let upid_str
= WorkerTask
::new_thread(
851 "barcode-label-media",
856 barcode_label_media_worker(worker
, drive
, pool
)
863 fn barcode_label_media_worker(
864 worker
: Arc
<WorkerTask
>,
866 pool
: Option
<String
>,
867 ) -> Result
<(), Error
> {
869 let (config
, _digest
) = config
::drive
::config()?
;
871 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
873 let label_text_list
= changer
.online_media_label_texts()?
;
875 let state_path
= Path
::new(TAPE_STATUS_DIR
);
877 let mut inventory
= Inventory
::load(state_path
)?
;
879 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
881 if label_text_list
.is_empty() {
882 bail
!("changer device does not list any media labels");
885 for label_text
in label_text_list
{
886 if label_text
.starts_with("CLN") { continue; }
889 if inventory
.find_media_by_label_text(&label_text
).is_some() {
890 worker
.log(format
!("media '{}' already inventoried (already labeled)", label_text
));
894 worker
.log(format
!("checking/loading media '{}'", label_text
));
896 if let Err(err
) = changer
.load_media(&label_text
) {
897 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
901 let mut drive
= open_drive(&config
, &drive
)?
;
904 match drive
.read_next_file() {
906 worker
.log(format
!("media '{}' is not empty (erase first)", label_text
));
909 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
911 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
912 /* assume tape is empty */
914 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", label_text
));
920 let ctime
= proxmox
::tools
::time
::epoch_i64();
921 let label
= MediaLabel
{
922 label_text
: label_text
.to_string(),
923 uuid
: Uuid
::generate(),
927 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
937 schema
: DRIVE_NAME_SCHEMA
,
942 description
: "A List of medium auxiliary memory attributes.",
949 /// Read Cartridge Memory (Medium auxiliary memory attributes)
950 pub fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
952 let (config
, _digest
) = config
::drive
::config()?
;
954 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
955 let mut handle
= drive_config
.open()?
;
957 handle
.cartridge_memory()
964 schema
: DRIVE_NAME_SCHEMA
,
969 type: Lp17VolumeStatistics
,
972 /// Read Volume Statistics (SCSI log page 17h)
973 pub fn volume_statistics(drive
: String
) -> Result
<Lp17VolumeStatistics
, Error
> {
975 let (config
, _digest
) = config
::drive
::config()?
;
977 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
978 let mut handle
= drive_config
.open()?
;
980 handle
.volume_statistics()
987 schema
: DRIVE_NAME_SCHEMA
,
992 type: LinuxDriveAndMediaStatus
,
995 /// Get drive/media status
996 pub fn status(drive
: String
) -> Result
<LinuxDriveAndMediaStatus
, Error
> {
998 let (config
, _digest
) = config
::drive
::config()?
;
1000 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
1002 // Note: use open_linux_tape_device, because this also works if no medium loaded
1003 let file
= open_linux_tape_device(&drive_config
.path
)?
;
1005 let mut handle
= LinuxTapeHandle
::new(file
);
1007 handle
.get_drive_and_media_status()
1014 schema
: DRIVE_NAME_SCHEMA
,
1017 description
: "Force overriding existing index.",
1022 description
: "Verbose mode - log all found chunks.",
1029 schema
: UPID_SCHEMA
,
1032 /// Scan media and record content
1033 pub fn catalog_media(
1035 force
: Option
<bool
>,
1036 verbose
: Option
<bool
>,
1037 rpcenv
: &mut dyn RpcEnvironment
,
1038 ) -> Result
<Value
, Error
> {
1040 let verbose
= verbose
.unwrap_or(false);
1041 let force
= force
.unwrap_or(false);
1043 let (config
, _digest
) = config
::drive
::config()?
;
1045 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
1047 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1049 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
1051 let upid_str
= WorkerTask
::new_thread(
1053 Some(drive
.clone()),
1058 let mut drive
= open_drive(&config
, &drive
)?
;
1062 let media_id
= match drive
.read_label()?
{
1063 (Some(media_id
), key_config
) => {
1065 "found media label: {}",
1066 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1068 if key_config
.is_some() {
1070 "encryption key config: {}",
1071 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1076 (None
, _
) => bail
!("media is empty (no media label found)"),
1079 let status_path
= Path
::new(TAPE_STATUS_DIR
);
1081 let mut inventory
= Inventory
::load(status_path
)?
;
1082 inventory
.store(media_id
.clone(), false)?
;
1084 let pool
= match media_id
.media_set_label
{
1086 worker
.log("media is empty");
1087 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1091 if set
.uuid
.as_ref() == [0u8;16] { // media is empty
1092 worker
.log("media is empty");
1093 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1096 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
1097 .map(|fp
| (fp
, set
.uuid
.clone()));
1099 drive
.set_encryption(encrypt_fingerprint
)?
;
1105 let _lock
= MediaPool
::lock(status_path
, &pool
)?
;
1107 if MediaCatalog
::exists(status_path
, &media_id
.label
.uuid
) && !force
{
1108 bail
!("media catalog exists (please use --force to overwrite)");
1111 restore_media(&worker
, &mut drive
, &media_id
, None
, verbose
)?
;
1125 schema
: CHANGER_NAME_SCHEMA
,
1131 description
: "The list of configured drives with model information.",
1134 type: DriveListEntry
,
1140 changer
: Option
<String
>,
1142 ) -> Result
<Vec
<DriveListEntry
>, Error
> {
1144 let (config
, _
) = config
::drive
::config()?
;
1146 let linux_drives
= linux_tape_device_list();
1148 let drive_list
: Vec
<LinuxTapeDrive
> = config
.convert_to_typed_array("linux")?
;
1150 let mut list
= Vec
::new();
1152 for drive
in drive_list
{
1153 if changer
.is_some() && drive
.changer
!= changer
{
1157 let mut entry
= DriveListEntry
{
1159 path
: drive
.path
.clone(),
1160 changer
: drive
.changer
,
1161 changer_drivenum
: drive
.changer_drivenum
,
1166 if let Some(info
) = lookup_drive(&linux_drives
, &drive
.path
) {
1167 entry
.vendor
= Some(info
.vendor
.clone());
1168 entry
.model
= Some(info
.model
.clone());
1169 entry
.serial
= Some(info
.serial
.clone());
1179 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1181 "barcode-label-media",
1183 .put(&API_METHOD_BARCODE_LABEL_MEDIA
)
1188 .put(&API_METHOD_CATALOG_MEDIA
)
1193 .put(&API_METHOD_CLEAN_DRIVE
)
1198 .post(&API_METHOD_EJECT_MEDIA
)
1203 .post(&API_METHOD_ERASE_MEDIA
)
1208 .get(&API_METHOD_INVENTORY
)
1209 .put(&API_METHOD_UPDATE_INVENTORY
)
1214 .put(&API_METHOD_LABEL_MEDIA
)
1219 .put(&API_METHOD_LOAD_MEDIA
)
1224 .put(&API_METHOD_LOAD_SLOT
)
1229 .get(&API_METHOD_CARTRIDGE_MEMORY
)
1232 "volume-statistics",
1234 .get(&API_METHOD_VOLUME_STATISTICS
)
1239 .get(&API_METHOD_READ_LABEL
)
1244 .post(&API_METHOD_REWIND
)
1249 .get(&API_METHOD_STATUS
)
1254 .put(&API_METHOD_UNLOAD
)
1258 const ITEM_ROUTER
: Router
= Router
::new()
1259 .get(&list_subdirs_api_method
!(SUBDIRS
))
1262 pub const ROUTER
: Router
= Router
::new()
1263 .get(&API_METHOD_LIST_DRIVES
)
1264 .match_all("drive", &ITEM_ROUTER
);