4 use anyhow
::{bail, Error}
;
10 list_subdirs_api_method
,
25 drive
::check_drive_exists
,
33 MEDIA_POOL_NAME_SCHEMA
,
40 LinuxDriveAndMediaStatus
,
42 tape
::restore
::restore_media
,
51 linux_tape_device_list
,
52 lookup_device_identification
,
61 open_linux_tape_device
,
63 required_media_changer
,
66 changer
::update_changer_online_status
,
74 schema
: DRIVE_NAME_SCHEMA
,
77 schema
: MEDIA_LABEL_SCHEMA
,
82 /// Load media with specified label
84 /// Issue a media load request to the associated changer device.
85 pub async
fn load_media(drive
: String
, label_text
: String
) -> Result
<(), Error
> {
87 let (config
, _digest
) = config
::drive
::config()?
;
89 tokio
::task
::spawn_blocking(move || {
90 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
91 changer
.load_media(&label_text
)
99 schema
: DRIVE_NAME_SCHEMA
,
102 description
: "Source slot number.",
108 /// Load media from the specified slot
110 /// Issue a media load request to the associated changer device.
111 pub async
fn load_slot(drive
: String
, source_slot
: u64) -> Result
<(), Error
> {
113 let (config
, _digest
) = config
::drive
::config()?
;
115 tokio
::task
::spawn_blocking(move || {
116 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
117 changer
.load_media_from_slot(source_slot
)
125 schema
: DRIVE_NAME_SCHEMA
,
128 schema
: MEDIA_LABEL_SCHEMA
,
133 description
: "The import-export slot number the media was transfered to.",
138 /// Export media with specified label
139 pub async
fn export_media(drive
: String
, label_text
: String
) -> Result
<u64, Error
> {
141 let (config
, _digest
) = config
::drive
::config()?
;
143 tokio
::task
::spawn_blocking(move || {
144 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
145 match changer
.export_media(&label_text
)?
{
146 Some(slot
) => Ok(slot
),
147 None
=> bail
!("media '{}' is not online (via changer '{}')", label_text
, changer_name
),
156 schema
: DRIVE_NAME_SCHEMA
,
159 description
: "Target slot number. If omitted, defaults to the slot that the drive was loaded from.",
166 /// Unload media via changer
169 target_slot
: Option
<u64>,
171 ) -> Result
<(), Error
> {
173 let (config
, _digest
) = config
::drive
::config()?
;
175 tokio
::task
::spawn_blocking(move || {
176 let (mut changer
, _
) = required_media_changer(&config
, &drive
)?
;
177 changer
.unload_media(target_slot
)
185 schema
: DRIVE_NAME_SCHEMA
,
188 description
: "Use fast erase.",
203 rpcenv
: &mut dyn RpcEnvironment
,
204 ) -> Result
<Value
, Error
> {
206 let (config
, _digest
) = config
::drive
::config()?
;
208 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
210 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
212 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
214 let upid_str
= WorkerTask
::new_thread(
220 let mut drive
= open_drive(&config
, &drive
)?
;
221 drive
.erase_media(fast
.unwrap_or(true))?
;
233 schema
: DRIVE_NAME_SCHEMA
,
244 rpcenv
: &mut dyn RpcEnvironment
,
245 ) -> Result
<Value
, Error
> {
247 let (config
, _digest
) = config
::drive
::config()?
;
249 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
251 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
253 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
255 let upid_str
= WorkerTask
::new_thread(
261 let mut drive
= open_drive(&config
, &drive
)?
;
274 schema
: DRIVE_NAME_SCHEMA
,
282 /// Eject/Unload drive media
285 rpcenv
: &mut dyn RpcEnvironment
,
286 ) -> Result
<Value
, Error
> {
288 let (config
, _digest
) = config
::drive
::config()?
;
290 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
292 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
294 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
296 let upid_str
= WorkerTask
::new_thread(
302 if let Some((mut changer
, _
)) = media_changer(&config
, &drive
)?
{
303 changer
.unload_media(None
)?
;
305 let mut drive
= open_drive(&config
, &drive
)?
;
306 drive
.eject_media()?
;
318 schema
: DRIVE_NAME_SCHEMA
,
321 schema
: MEDIA_LABEL_SCHEMA
,
324 schema
: MEDIA_POOL_NAME_SCHEMA
,
335 /// Write a new media label to the media in 'drive'. The media is
336 /// assigned to the specified 'pool', or else to the free media pool.
338 /// Note: The media need to be empty (you may want to erase it first).
341 pool
: Option
<String
>,
343 rpcenv
: &mut dyn RpcEnvironment
,
344 ) -> Result
<Value
, Error
> {
346 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
348 if let Some(ref pool
) = pool
{
349 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
351 if pool_config
.sections
.get(pool
).is_none() {
352 bail
!("no such pool ('{}')", pool
);
356 let (config
, _digest
) = config
::drive
::config()?
;
358 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
360 let upid_str
= WorkerTask
::new_thread(
367 let mut drive
= open_drive(&config
, &drive
)?
;
371 match drive
.read_next_file() {
372 Ok(Some(_file
)) => bail
!("media is not empty (erase first)"),
373 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
375 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
376 /* assume tape is empty */
378 bail
!("media read error - {}", err
);
383 let ctime
= proxmox
::tools
::time
::epoch_i64();
384 let label
= MediaLabel
{
385 label_text
: label_text
.to_string(),
386 uuid
: Uuid
::generate(),
390 write_media_label(worker
, &mut drive
, label
, pool
)
397 fn write_media_label(
398 worker
: Arc
<WorkerTask
>,
399 drive
: &mut Box
<dyn TapeDriver
>,
401 pool
: Option
<String
>,
402 ) -> Result
<(), Error
> {
404 drive
.label_tape(&label
)?
;
406 let mut media_set_label
= None
;
408 if let Some(ref pool
) = pool
{
409 // assign media to pool by writing special media set label
410 worker
.log(format
!("Label media '{}' for pool '{}'", label
.label_text
, pool
));
411 let set
= MediaSetLabel
::with_data(&pool
, [0u8; 16].into(), 0, label
.ctime
, None
);
413 drive
.write_media_set_label(&set
, None
)?
;
414 media_set_label
= Some(set
);
416 worker
.log(format
!("Label media '{}' (no pool assignment)", label
.label_text
));
419 let media_id
= MediaId { label, media_set_label }
;
421 let status_path
= Path
::new(TAPE_STATUS_DIR
);
423 // Create the media catalog
424 MediaCatalog
::overwrite(status_path
, &media_id
, false)?
;
426 let mut inventory
= Inventory
::load(status_path
)?
;
427 inventory
.store(media_id
.clone(), false)?
;
431 match drive
.read_label() {
432 Ok((Some(info
), _
)) => {
433 if info
.label
.uuid
!= media_id
.label
.uuid
{
434 bail
!("verify label failed - got wrong label uuid");
436 if let Some(ref pool
) = pool
{
437 match info
.media_set_label
{
439 if set
.uuid
!= [0u8; 16].into() {
440 bail
!("verify media set label failed - got wrong set uuid");
442 if &set
.pool
!= pool
{
443 bail
!("verify media set label failed - got wrong pool");
447 bail
!("verify media set label failed (missing set label)");
452 Ok((None
, _
)) => bail
!("verify label failed (got empty media)"),
453 Err(err
) => bail
!("verify label failed - {}", err
),
466 schema
: DRIVE_NAME_SCHEMA
,
469 description
: "Encryption key password.",
474 /// Try to restore a tape encryption key
475 pub async
fn restore_key(
478 ) -> Result
<(), Error
> {
480 let (config
, _digest
) = config
::drive
::config()?
;
482 tokio
::task
::spawn_blocking(move || {
483 let mut drive
= open_drive(&config
, &drive
)?
;
485 let (_media_id
, key_config
) = drive
.read_label()?
;
487 if let Some(key_config
) = key_config
{
488 let password_fn
= || { Ok(password.as_bytes().to_vec()) }
;
489 let (key
, ..) = key_config
.decrypt(&password_fn
)?
;
490 config
::tape_encryption_keys
::insert_key(key
, key_config
, true)?
;
492 bail
!("media does not contain any encryption key configuration");
503 schema
: DRIVE_NAME_SCHEMA
,
506 description
: "Inventorize media",
515 /// Read media label (optionally inventorize media)
516 pub async
fn read_label(
518 inventorize
: Option
<bool
>,
519 ) -> Result
<MediaIdFlat
, Error
> {
521 let (config
, _digest
) = config
::drive
::config()?
;
523 tokio
::task
::spawn_blocking(move || {
524 let mut drive
= open_drive(&config
, &drive
)?
;
526 let (media_id
, _key_config
) = drive
.read_label()?
;
528 let media_id
= match media_id
{
530 let mut flat
= MediaIdFlat
{
531 uuid
: media_id
.label
.uuid
.clone(),
532 label_text
: media_id
.label
.label_text
.clone(),
533 ctime
: media_id
.label
.ctime
,
534 media_set_ctime
: None
,
535 media_set_uuid
: None
,
536 encryption_key_fingerprint
: None
,
540 if let Some(ref set
) = media_id
.media_set_label
{
541 flat
.pool
= Some(set
.pool
.clone());
542 flat
.seq_nr
= Some(set
.seq_nr
);
543 flat
.media_set_uuid
= Some(set
.uuid
.clone());
544 flat
.media_set_ctime
= Some(set
.ctime
);
545 flat
.encryption_key_fingerprint
= set
546 .encryption_key_fingerprint
548 .map(|fp
| crate::tools
::format
::as_fingerprint(fp
.bytes()));
550 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
551 .map(|fp
| (fp
, set
.uuid
.clone()));
553 if let Err(err
) = drive
.set_encryption(encrypt_fingerprint
) {
554 // try, but ignore errors. just log to stderr
555 eprintln
!("uable to load encryption key: {}", err
);
559 if let Some(true) = inventorize
{
560 let state_path
= Path
::new(TAPE_STATUS_DIR
);
561 let mut inventory
= Inventory
::load(state_path
)?
;
562 inventory
.store(media_id
, false)?
;
568 bail
!("Media is empty (no label).");
580 schema
: DRIVE_NAME_SCHEMA
,
591 rpcenv
: &mut dyn RpcEnvironment
,
592 ) -> Result
<Value
, Error
> {
594 let (config
, _digest
) = config
::drive
::config()?
;
596 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
598 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
600 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
602 let upid_str
= WorkerTask
::new_thread(
609 let (mut changer
, _changer_name
) = required_media_changer(&config
, &drive
)?
;
611 worker
.log("Starting drive clean");
613 changer
.clean_drive()?
;
615 worker
.log("Drive cleaned sucessfully");
627 schema
: DRIVE_NAME_SCHEMA
,
632 description
: "The list of media labels with associated media Uuid (if any).",
639 /// List known media labels (Changer Inventory)
641 /// Note: Only useful for drives with associated changer device.
643 /// This method queries the changer to get a list of media labels.
645 /// Note: This updates the media online status.
646 pub async
fn inventory(
648 ) -> Result
<Vec
<LabelUuidMap
>, Error
> {
650 let (config
, _digest
) = config
::drive
::config()?
;
652 tokio
::task
::spawn_blocking(move || {
653 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
655 let label_text_list
= changer
.online_media_label_texts()?
;
657 let state_path
= Path
::new(TAPE_STATUS_DIR
);
659 let mut inventory
= Inventory
::load(state_path
)?
;
661 update_changer_online_status(
668 let mut list
= Vec
::new();
670 for label_text
in label_text_list
.iter() {
671 if label_text
.starts_with("CLN") {
672 // skip cleaning unit
676 let label_text
= label_text
.to_string();
678 if let Some(media_id
) = inventory
.find_media_by_label_text(&label_text
) {
679 list
.push(LabelUuidMap { label_text, uuid: Some(media_id.label.uuid.clone()) }
);
681 list
.push(LabelUuidMap { label_text, uuid: None }
);
693 schema
: DRIVE_NAME_SCHEMA
,
696 description
: "Load all tapes and try read labels (even if already inventoried)",
708 /// Note: Only useful for drives with associated changer device.
710 /// This method queries the changer to get a list of media labels. It
711 /// then loads any unknown media into the drive, reads the label, and
712 /// store the result to the media database.
714 /// Note: This updates the media online status.
715 pub fn update_inventory(
717 read_all_labels
: Option
<bool
>,
718 rpcenv
: &mut dyn RpcEnvironment
,
719 ) -> Result
<Value
, Error
> {
721 let (config
, _digest
) = config
::drive
::config()?
;
723 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
725 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
727 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
729 let upid_str
= WorkerTask
::new_thread(
736 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
738 let label_text_list
= changer
.online_media_label_texts()?
;
739 if label_text_list
.is_empty() {
740 worker
.log("changer device does not list any media labels".to_string());
743 let state_path
= Path
::new(TAPE_STATUS_DIR
);
745 let mut inventory
= Inventory
::load(state_path
)?
;
747 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
749 for label_text
in label_text_list
.iter() {
750 if label_text
.starts_with("CLN") {
751 worker
.log(format
!("skip cleaning unit '{}'", label_text
));
755 let label_text
= label_text
.to_string();
757 if !read_all_labels
.unwrap_or(false) && inventory
.find_media_by_label_text(&label_text
).is_some() {
758 worker
.log(format
!("media '{}' already inventoried", label_text
));
762 if let Err(err
) = changer
.load_media(&label_text
) {
763 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
767 let mut drive
= open_drive(&config
, &drive
)?
;
768 match drive
.read_label() {
770 worker
.warn(format
!("unable to read label form media '{}' - {}", label_text
, err
));
773 worker
.log(format
!("media '{}' is empty", label_text
));
775 Ok((Some(media_id
), _key_config
)) => {
776 if label_text
!= media_id
.label
.label_text
{
777 worker
.warn(format
!("label text missmatch ({} != {})", label_text
, media_id
.label
.label_text
));
780 worker
.log(format
!("inventorize media '{}' with uuid '{}'", label_text
, media_id
.label
.uuid
));
781 inventory
.store(media_id
, false)?
;
784 changer
.unload_media(None
)?
;
798 schema
: DRIVE_NAME_SCHEMA
,
801 schema
: MEDIA_POOL_NAME_SCHEMA
,
810 /// Label media with barcodes from changer device
811 pub fn barcode_label_media(
813 pool
: Option
<String
>,
814 rpcenv
: &mut dyn RpcEnvironment
,
815 ) -> Result
<Value
, Error
> {
817 if let Some(ref pool
) = pool
{
818 let (pool_config
, _digest
) = config
::media_pool
::config()?
;
820 if pool_config
.sections
.get(pool
).is_none() {
821 bail
!("no such pool ('{}')", pool
);
825 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
827 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
829 let upid_str
= WorkerTask
::new_thread(
830 "barcode-label-media",
835 barcode_label_media_worker(worker
, drive
, pool
)
842 fn barcode_label_media_worker(
843 worker
: Arc
<WorkerTask
>,
845 pool
: Option
<String
>,
846 ) -> Result
<(), Error
> {
848 let (config
, _digest
) = config
::drive
::config()?
;
850 let (mut changer
, changer_name
) = required_media_changer(&config
, &drive
)?
;
852 let label_text_list
= changer
.online_media_label_texts()?
;
854 let state_path
= Path
::new(TAPE_STATUS_DIR
);
856 let mut inventory
= Inventory
::load(state_path
)?
;
858 update_changer_online_status(&config
, &mut inventory
, &changer_name
, &label_text_list
)?
;
860 if label_text_list
.is_empty() {
861 bail
!("changer device does not list any media labels");
864 for label_text
in label_text_list
{
865 if label_text
.starts_with("CLN") { continue; }
868 if inventory
.find_media_by_label_text(&label_text
).is_some() {
869 worker
.log(format
!("media '{}' already inventoried (already labeled)", label_text
));
873 worker
.log(format
!("checking/loading media '{}'", label_text
));
875 if let Err(err
) = changer
.load_media(&label_text
) {
876 worker
.warn(format
!("unable to load media '{}' - {}", label_text
, err
));
880 let mut drive
= open_drive(&config
, &drive
)?
;
883 match drive
.read_next_file() {
885 worker
.log(format
!("media '{}' is not empty (erase first)", label_text
));
888 Ok(None
) => { /* EOF mark at BOT, assume tape is empty */ }
,
890 if err
.is_errno(nix
::errno
::Errno
::ENOSPC
) || err
.is_errno(nix
::errno
::Errno
::EIO
) {
891 /* assume tape is empty */
893 worker
.warn(format
!("media '{}' read error (maybe not empty - erase first)", label_text
));
899 let ctime
= proxmox
::tools
::time
::epoch_i64();
900 let label
= MediaLabel
{
901 label_text
: label_text
.to_string(),
902 uuid
: Uuid
::generate(),
906 write_media_label(worker
.clone(), &mut drive
, label
, pool
.clone())?
916 schema
: DRIVE_NAME_SCHEMA
,
921 description
: "A List of medium auxiliary memory attributes.",
928 /// Read Cartridge Memory (Medium auxiliary memory attributes)
929 pub fn cartridge_memory(drive
: String
) -> Result
<Vec
<MamAttribute
>, Error
> {
931 let (config
, _digest
) = config
::drive
::config()?
;
933 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
934 let mut handle
= drive_config
.open()?
;
936 handle
.cartridge_memory()
943 schema
: DRIVE_NAME_SCHEMA
,
948 type: Lp17VolumeStatistics
,
951 /// Read Volume Statistics (SCSI log page 17h)
952 pub fn volume_statistics(drive
: String
) -> Result
<Lp17VolumeStatistics
, Error
> {
954 let (config
, _digest
) = config
::drive
::config()?
;
956 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
957 let mut handle
= drive_config
.open()?
;
959 handle
.volume_statistics()
966 schema
: DRIVE_NAME_SCHEMA
,
971 type: LinuxDriveAndMediaStatus
,
974 /// Get drive/media status
975 pub fn status(drive
: String
) -> Result
<LinuxDriveAndMediaStatus
, Error
> {
977 let (config
, _digest
) = config
::drive
::config()?
;
979 let drive_config
: LinuxTapeDrive
= config
.lookup("linux", &drive
)?
;
981 // Note: use open_linux_tape_device, because this also works if no medium loaded
982 let file
= open_linux_tape_device(&drive_config
.path
)?
;
984 let mut handle
= LinuxTapeHandle
::new(file
);
986 handle
.get_drive_and_media_status()
993 schema
: DRIVE_NAME_SCHEMA
,
996 description
: "Force overriding existing index.",
1001 description
: "Verbose mode - log all found chunks.",
1008 schema
: UPID_SCHEMA
,
1011 /// Scan media and record content
1012 pub fn catalog_media(
1014 force
: Option
<bool
>,
1015 verbose
: Option
<bool
>,
1016 rpcenv
: &mut dyn RpcEnvironment
,
1017 ) -> Result
<Value
, Error
> {
1019 let verbose
= verbose
.unwrap_or(false);
1020 let force
= force
.unwrap_or(false);
1022 let (config
, _digest
) = config
::drive
::config()?
;
1024 check_drive_exists(&config
, &drive
)?
; // early check before starting worker
1026 let auth_id
: Authid
= rpcenv
.get_auth_id().unwrap().parse()?
;
1028 let to_stdout
= rpcenv
.env_type() == RpcEnvironmentType
::CLI
;
1030 let upid_str
= WorkerTask
::new_thread(
1032 Some(drive
.clone()),
1037 let mut drive
= open_drive(&config
, &drive
)?
;
1041 let media_id
= match drive
.read_label()?
{
1042 (Some(media_id
), key_config
) => {
1044 "found media label: {}",
1045 serde_json
::to_string_pretty(&serde_json
::to_value(&media_id
)?
)?
1047 if key_config
.is_some() {
1049 "encryption key config: {}",
1050 serde_json
::to_string_pretty(&serde_json
::to_value(&key_config
)?
)?
1055 (None
, _
) => bail
!("media is empty (no media label found)"),
1058 let status_path
= Path
::new(TAPE_STATUS_DIR
);
1060 let mut inventory
= Inventory
::load(status_path
)?
;
1061 inventory
.store(media_id
.clone(), false)?
;
1063 let pool
= match media_id
.media_set_label
{
1065 worker
.log("media is empty");
1066 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1070 if set
.uuid
.as_ref() == [0u8;16] { // media is empty
1071 worker
.log("media is empty");
1072 MediaCatalog
::destroy(status_path
, &media_id
.label
.uuid
)?
;
1075 let encrypt_fingerprint
= set
.encryption_key_fingerprint
.clone()
1076 .map(|fp
| (fp
, set
.uuid
.clone()));
1078 drive
.set_encryption(encrypt_fingerprint
)?
;
1084 let _lock
= MediaPool
::lock(status_path
, &pool
)?
;
1086 if MediaCatalog
::exists(status_path
, &media_id
.label
.uuid
) && !force
{
1087 bail
!("media catalog exists (please use --force to overwrite)");
1090 restore_media(&worker
, &mut drive
, &media_id
, None
, verbose
)?
;
1104 schema
: CHANGER_NAME_SCHEMA
,
1110 description
: "The list of configured drives with model information.",
1113 type: DriveListEntry
,
1119 changer
: Option
<String
>,
1121 ) -> Result
<Vec
<DriveListEntry
>, Error
> {
1123 let (config
, _
) = config
::drive
::config()?
;
1125 let linux_drives
= linux_tape_device_list();
1127 let drive_list
: Vec
<LinuxTapeDrive
> = config
.convert_to_typed_array("linux")?
;
1129 let mut list
= Vec
::new();
1131 for drive
in drive_list
{
1132 if changer
.is_some() && drive
.changer
!= changer
{
1136 let info
= lookup_device_identification(&linux_drives
, &drive
.path
);
1137 let entry
= DriveListEntry { config: drive, info }
;
1145 pub const SUBDIRS
: SubdirMap
= &sorted
!([
1147 "barcode-label-media",
1149 .post(&API_METHOD_BARCODE_LABEL_MEDIA
)
1154 .post(&API_METHOD_CATALOG_MEDIA
)
1159 .put(&API_METHOD_CLEAN_DRIVE
)
1164 .post(&API_METHOD_EJECT_MEDIA
)
1169 .post(&API_METHOD_ERASE_MEDIA
)
1174 .put(&API_METHOD_EXPORT_MEDIA
)
1179 .get(&API_METHOD_INVENTORY
)
1180 .put(&API_METHOD_UPDATE_INVENTORY
)
1185 .post(&API_METHOD_LABEL_MEDIA
)
1190 .put(&API_METHOD_LOAD_MEDIA
)
1195 .put(&API_METHOD_LOAD_SLOT
)
1200 .get(&API_METHOD_CARTRIDGE_MEMORY
)
1203 "volume-statistics",
1205 .get(&API_METHOD_VOLUME_STATISTICS
)
1210 .get(&API_METHOD_READ_LABEL
)
1215 .post(&API_METHOD_REWIND
)
1220 .get(&API_METHOD_STATUS
)
1225 .put(&API_METHOD_UNLOAD
)
1229 const ITEM_ROUTER
: Router
= Router
::new()
1230 .get(&list_subdirs_api_method
!(SUBDIRS
))
1233 pub const ROUTER
: Router
= Router
::new()
1234 .get(&API_METHOD_LIST_DRIVES
)
1235 .match_all("drive", &ITEM_ROUTER
);