tape: rust fmt
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
@ -9,15 +9,14 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
|
||||
use proxmox_sys::fs::{CreateOptions, replace_file, file_read_optional_string};
|
||||
use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions};
|
||||
|
||||
use pbs_api_types::{ScsiTapeChanger, LtoTapeDrive};
|
||||
use pbs_api_types::{LtoTapeDrive, ScsiTapeChanger};
|
||||
|
||||
use pbs_tape::{sg_pt_changer, MtxStatus, ElementStatus};
|
||||
use pbs_tape::{sg_pt_changer, ElementStatus, MtxStatus};
|
||||
|
||||
/// Interface to SCSI changer devices
|
||||
pub trait ScsiMediaChange {
|
||||
|
||||
fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error>;
|
||||
|
||||
fn load_slot(&mut self, from_slot: u64, drivenum: u64) -> Result<MtxStatus, Error>;
|
||||
@ -29,7 +28,6 @@ pub trait ScsiMediaChange {
|
||||
|
||||
/// Interface to the media changer device for a single drive
|
||||
pub trait MediaChange {
|
||||
|
||||
/// Drive number inside changer
|
||||
fn drive_number(&self) -> u64;
|
||||
|
||||
@ -55,9 +53,11 @@ pub trait MediaChange {
|
||||
/// slots. Also, you cannot load cleaning units with this
|
||||
/// interface.
|
||||
fn load_media(&mut self, label_text: &str) -> Result<MtxStatus, Error> {
|
||||
|
||||
if label_text.starts_with("CLN") {
|
||||
bail!("unable to load media '{}' (seems to be a cleaning unit)", label_text);
|
||||
bail!(
|
||||
"unable to load media '{}' (seems to be a cleaning unit)",
|
||||
label_text
|
||||
);
|
||||
}
|
||||
|
||||
let mut status = self.status()?;
|
||||
@ -69,17 +69,21 @@ pub trait MediaChange {
|
||||
if let ElementStatus::VolumeTag(ref tag) = drive_status.status {
|
||||
if *tag == label_text {
|
||||
if i as u64 != self.drive_number() {
|
||||
bail!("unable to load media '{}' - media in wrong drive ({} != {})",
|
||||
label_text, i, self.drive_number());
|
||||
bail!(
|
||||
"unable to load media '{}' - media in wrong drive ({} != {})",
|
||||
label_text,
|
||||
i,
|
||||
self.drive_number()
|
||||
);
|
||||
}
|
||||
return Ok(status) // already loaded
|
||||
return Ok(status); // already loaded
|
||||
}
|
||||
}
|
||||
if i as u64 == self.drive_number() {
|
||||
match drive_status.status {
|
||||
ElementStatus::Empty => { /* OK */ },
|
||||
ElementStatus::Empty => { /* OK */ }
|
||||
_ => unload_drive = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,9 +96,12 @@ pub trait MediaChange {
|
||||
if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
|
||||
if tag == label_text {
|
||||
if slot_info.import_export {
|
||||
bail!("unable to load media '{}' - inside import/export slot", label_text);
|
||||
bail!(
|
||||
"unable to load media '{}' - inside import/export slot",
|
||||
label_text
|
||||
);
|
||||
}
|
||||
slot = Some(i+1);
|
||||
slot = Some(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -127,9 +134,13 @@ pub trait MediaChange {
|
||||
}
|
||||
|
||||
for slot_info in status.slots.iter() {
|
||||
if slot_info.import_export { continue; }
|
||||
if slot_info.import_export {
|
||||
continue;
|
||||
}
|
||||
if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
|
||||
if tag.starts_with("CLN") { continue; }
|
||||
if tag.starts_with("CLN") {
|
||||
continue;
|
||||
}
|
||||
list.push(tag.clone());
|
||||
}
|
||||
}
|
||||
@ -147,15 +158,19 @@ pub trait MediaChange {
|
||||
// Unload drive first. Note: This also unloads a loaded cleaning tape
|
||||
if let Some(drive_status) = status.drives.get(self.drive_number() as usize) {
|
||||
match drive_status.status {
|
||||
ElementStatus::Empty => { /* OK */ },
|
||||
_ => { status = self.unload_to_free_slot(status)?; }
|
||||
ElementStatus::Empty => { /* OK */ }
|
||||
_ => {
|
||||
status = self.unload_to_free_slot(status)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cleaning_cartridge_slot = None;
|
||||
|
||||
for (i, slot_info) in status.slots.iter().enumerate() {
|
||||
if slot_info.import_export { continue; }
|
||||
if slot_info.import_export {
|
||||
continue;
|
||||
}
|
||||
if let ElementStatus::VolumeTag(ref tag) = slot_info.status {
|
||||
if tag.starts_with("CLN") {
|
||||
cleaning_cartridge_slot = Some(i + 1);
|
||||
@ -169,7 +184,6 @@ pub trait MediaChange {
|
||||
Some(cleaning_cartridge_slot) => cleaning_cartridge_slot as u64,
|
||||
};
|
||||
|
||||
|
||||
self.load_media_from_slot(cleaning_cartridge_slot)?;
|
||||
|
||||
self.unload_media(Some(cleaning_cartridge_slot))
|
||||
@ -197,7 +211,9 @@ pub trait MediaChange {
|
||||
|
||||
for (i, slot_info) in status.slots.iter().enumerate() {
|
||||
if slot_info.import_export {
|
||||
if to.is_some() { continue; }
|
||||
if to.is_some() {
|
||||
continue;
|
||||
}
|
||||
if let ElementStatus::Empty = slot_info.status {
|
||||
to = Some(i as u64 + 1);
|
||||
}
|
||||
@ -214,7 +230,7 @@ pub trait MediaChange {
|
||||
self.unload_media(Some(to))?;
|
||||
Ok(Some(to))
|
||||
}
|
||||
None => bail!("unable to find free export slot"),
|
||||
None => bail!("unable to find free export slot"),
|
||||
}
|
||||
} else {
|
||||
match (from, to) {
|
||||
@ -234,7 +250,6 @@ pub trait MediaChange {
|
||||
///
|
||||
/// Note: This method consumes status - so please use returned status afterward.
|
||||
fn unload_to_free_slot(&mut self, status: MtxStatus) -> Result<MtxStatus, Error> {
|
||||
|
||||
let drive_status = &status.drives[self.drive_number() as usize];
|
||||
if let Some(slot) = drive_status.loaded_slot {
|
||||
// check if original slot is empty/usable
|
||||
@ -248,7 +263,10 @@ pub trait MediaChange {
|
||||
if let Some(slot) = status.find_free_slot(false) {
|
||||
self.unload_media(Some(slot))
|
||||
} else {
|
||||
bail!("drive '{}' unload failure - no free slot", self.drive_name());
|
||||
bail!(
|
||||
"drive '{}' unload failure - no free slot",
|
||||
self.drive_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -256,8 +274,7 @@ pub trait MediaChange {
|
||||
const USE_MTX: bool = false;
|
||||
|
||||
impl ScsiMediaChange for ScsiTapeChanger {
|
||||
|
||||
fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
|
||||
fn status(&mut self, use_cache: bool) -> Result<MtxStatus, Error> {
|
||||
if use_cache {
|
||||
if let Some(state) = load_changer_state_cache(&self.name)? {
|
||||
return Ok(state);
|
||||
@ -328,11 +345,7 @@ impl ScsiMediaChange for ScsiTapeChanger {
|
||||
}
|
||||
}
|
||||
|
||||
fn save_changer_state_cache(
|
||||
changer: &str,
|
||||
state: &MtxStatus,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
fn save_changer_state_cache(changer: &str, state: &MtxStatus) -> Result<(), Error> {
|
||||
let mut path = PathBuf::from(crate::tape::CHANGER_STATE_DIR);
|
||||
path.push(changer);
|
||||
|
||||
@ -377,7 +390,6 @@ pub struct MtxMediaChanger {
|
||||
}
|
||||
|
||||
impl MtxMediaChanger {
|
||||
|
||||
pub fn with_drive_config(drive_config: &LtoTapeDrive) -> Result<Self, Error> {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
let changer_config: ScsiTapeChanger = match drive_config.changer {
|
||||
@ -394,7 +406,6 @@ impl MtxMediaChanger {
|
||||
}
|
||||
|
||||
impl MediaChange for MtxMediaChanger {
|
||||
|
||||
fn drive_number(&self) -> u64 {
|
||||
self.drive_number
|
||||
}
|
||||
|
@ -1,14 +1,10 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use proxmox_sys::command::run_command;
|
||||
use pbs_api_types::ScsiTapeChanger;
|
||||
use pbs_tape::MtxStatus;
|
||||
use proxmox_sys::command::run_command;
|
||||
|
||||
use crate::{
|
||||
tape::changer::{
|
||||
mtx::parse_mtx_status,
|
||||
},
|
||||
};
|
||||
use crate::tape::changer::mtx::parse_mtx_status;
|
||||
|
||||
/// Run 'mtx status' and return parsed result.
|
||||
pub fn mtx_status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
|
||||
@ -27,12 +23,7 @@ pub fn mtx_status(config: &ScsiTapeChanger) -> Result<MtxStatus, Error> {
|
||||
}
|
||||
|
||||
/// Run 'mtx load'
|
||||
pub fn mtx_load(
|
||||
path: &str,
|
||||
slot: u64,
|
||||
drivenum: u64,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
pub fn mtx_load(path: &str, slot: u64, drivenum: u64) -> Result<(), Error> {
|
||||
let mut command = std::process::Command::new("mtx");
|
||||
command.args(&["-f", path, "load", &slot.to_string(), &drivenum.to_string()]);
|
||||
run_command(command, None)?;
|
||||
@ -41,28 +32,30 @@ pub fn mtx_load(
|
||||
}
|
||||
|
||||
/// Run 'mtx unload'
|
||||
pub fn mtx_unload(
|
||||
path: &str,
|
||||
slot: u64,
|
||||
drivenum: u64,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
pub fn mtx_unload(path: &str, slot: u64, drivenum: u64) -> Result<(), Error> {
|
||||
let mut command = std::process::Command::new("mtx");
|
||||
command.args(&["-f", path, "unload", &slot.to_string(), &drivenum.to_string()]);
|
||||
command.args(&[
|
||||
"-f",
|
||||
path,
|
||||
"unload",
|
||||
&slot.to_string(),
|
||||
&drivenum.to_string(),
|
||||
]);
|
||||
run_command(command, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run 'mtx transfer'
|
||||
pub fn mtx_transfer(
|
||||
path: &str,
|
||||
from_slot: u64,
|
||||
to_slot: u64,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
pub fn mtx_transfer(path: &str, from_slot: u64, to_slot: u64) -> Result<(), Error> {
|
||||
let mut command = std::process::Command::new("mtx");
|
||||
command.args(&["-f", path, "transfer", &from_slot.to_string(), &to_slot.to_string()]);
|
||||
command.args(&[
|
||||
"-f",
|
||||
path,
|
||||
"transfer",
|
||||
&from_slot.to_string(),
|
||||
&to_slot.to_string(),
|
||||
]);
|
||||
|
||||
run_command(command, None)?;
|
||||
|
||||
|
@ -1,17 +1,15 @@
|
||||
use anyhow::Error;
|
||||
|
||||
use nom::bytes::complete::{take_while, tag};
|
||||
use nom::bytes::complete::{tag, take_while};
|
||||
|
||||
use pbs_tape::{ElementStatus, MtxStatus, DriveStatus, StorageElementStatus};
|
||||
use pbs_tape::{DriveStatus, ElementStatus, MtxStatus, StorageElementStatus};
|
||||
|
||||
use pbs_tools::nom::{
|
||||
parse_complete, multispace0, multispace1, parse_u64,
|
||||
parse_failure, parse_error, IResult,
|
||||
multispace0, multispace1, parse_complete, parse_error, parse_failure, parse_u64, IResult,
|
||||
};
|
||||
|
||||
|
||||
// Recognizes one line
|
||||
fn next_line(i: &str) -> IResult<&str, &str> {
|
||||
fn next_line(i: &str) -> IResult<&str, &str> {
|
||||
let (i, line) = take_while(|c| (c != '\n'))(i)?;
|
||||
if i.is_empty() {
|
||||
Ok((i, line))
|
||||
@ -21,7 +19,6 @@ fn next_line(i: &str) -> IResult<&str, &str> {
|
||||
}
|
||||
|
||||
fn parse_storage_changer(i: &str) -> IResult<&str, ()> {
|
||||
|
||||
let (i, _) = multispace0(i)?;
|
||||
let (i, _) = tag("Storage Changer")(i)?;
|
||||
let (i, _) = next_line(i)?; // skip
|
||||
@ -30,7 +27,6 @@ fn parse_storage_changer(i: &str) -> IResult<&str, ()> {
|
||||
}
|
||||
|
||||
fn parse_drive_status(i: &str, id: u64) -> IResult<&str, DriveStatus> {
|
||||
|
||||
let mut loaded_slot = None;
|
||||
|
||||
if let Some(empty) = i.strip_prefix("Empty") {
|
||||
@ -87,14 +83,13 @@ fn parse_drive_status(i: &str, id: u64) -> IResult<&str, DriveStatus> {
|
||||
|
||||
fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> {
|
||||
if let Some(empty) = i.strip_prefix("Empty") {
|
||||
return Ok((empty, ElementStatus::Empty));
|
||||
return Ok((empty, ElementStatus::Empty));
|
||||
}
|
||||
if let Some(n) = i.strip_prefix("Full ") {
|
||||
if let Some(n) = n.strip_prefix(":VolumeTag=") {
|
||||
let (n, tag) = take_while(|c| !(c == ' ' || c == ':' || c == '\n'))(n)?;
|
||||
let (n, _) = take_while(|c| c != '\n')(n)?; // skip to eol
|
||||
return Ok((n, ElementStatus::VolumeTag(tag.to_string())));
|
||||
|
||||
}
|
||||
let (n, _) = take_while(|c| c != '\n')(n)?; // skip
|
||||
|
||||
@ -105,7 +100,6 @@ fn parse_slot_status(i: &str) -> IResult<&str, ElementStatus> {
|
||||
}
|
||||
|
||||
fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> {
|
||||
|
||||
let (i, _) = tag("Data Transfer Element")(i)?;
|
||||
let (i, _) = multispace1(i)?;
|
||||
let (i, id) = parse_u64(i)?;
|
||||
@ -117,13 +111,12 @@ fn parse_data_transfer_element(i: &str) -> IResult<&str, (u64, DriveStatus)> {
|
||||
}
|
||||
|
||||
fn parse_storage_element(i: &str) -> IResult<&str, (u64, bool, ElementStatus)> {
|
||||
|
||||
let (i, _) = multispace1(i)?;
|
||||
let (i, _) = tag("Storage Element")(i)?;
|
||||
let (i, _) = multispace1(i)?;
|
||||
let (i, id) = parse_u64(i)?;
|
||||
let (i, opt_ie) = nom::combinator::opt(tag(" IMPORT/EXPORT"))(i)?;
|
||||
let import_export = opt_ie.is_some();
|
||||
let import_export = opt_ie.is_some();
|
||||
let (i, _) = nom::character::complete::char(':')(i)?;
|
||||
let (i, element_status) = parse_slot_status(i)?;
|
||||
let (i, _) = nom::character::complete::newline(i)?;
|
||||
@ -131,8 +124,7 @@ fn parse_storage_element(i: &str) -> IResult<&str, (u64, bool, ElementStatus)> {
|
||||
Ok((i, (id, import_export, element_status)))
|
||||
}
|
||||
|
||||
fn parse_status(i: &str) -> IResult<&str, MtxStatus> {
|
||||
|
||||
fn parse_status(i: &str) -> IResult<&str, MtxStatus> {
|
||||
let (mut i, _) = parse_storage_changer(i)?;
|
||||
|
||||
let mut drives = Vec::new();
|
||||
@ -158,14 +150,17 @@ fn parse_status(i: &str) -> IResult<&str, MtxStatus> {
|
||||
slots.push(status);
|
||||
}
|
||||
|
||||
let status = MtxStatus { drives, slots, transports: Vec::new() };
|
||||
let status = MtxStatus {
|
||||
drives,
|
||||
slots,
|
||||
transports: Vec::new(),
|
||||
};
|
||||
|
||||
Ok((i, status))
|
||||
}
|
||||
|
||||
/// Parses the output from 'mtx status'
|
||||
pub fn parse_mtx_status(i: &str) -> Result<MtxStatus, Error> {
|
||||
|
||||
let status = parse_complete("mtx status", i, parse_status)?;
|
||||
|
||||
Ok(status)
|
||||
@ -173,7 +168,6 @@ pub fn parse_mtx_status(i: &str) -> Result<MtxStatus, Error> {
|
||||
|
||||
#[test]
|
||||
fn test_changer_status() -> Result<(), Error> {
|
||||
|
||||
let output = r###" Storage Changer /dev/tape/by-id/scsi-387408F60F0000:2 Drives, 24 Slots ( 4 Import/Export )
|
||||
Data Transfer Element 0:Empty
|
||||
Data Transfer Element 1:Empty
|
||||
|
@ -1,16 +1,16 @@
|
||||
use std::path::Path;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
|
||||
use proxmox_section_config::SectionConfigData;
|
||||
use proxmox_uuid::Uuid;
|
||||
|
||||
use pbs_api_types::{VirtualTapeDrive, ScsiTapeChanger};
|
||||
use pbs_api_types::{ScsiTapeChanger, VirtualTapeDrive};
|
||||
use pbs_tape::{ElementStatus, MtxStatus};
|
||||
|
||||
use crate::tape::Inventory;
|
||||
use crate::tape::changer::{MediaChange, ScsiMediaChange};
|
||||
use crate::tape::Inventory;
|
||||
|
||||
/// Helper to update media online status
|
||||
///
|
||||
@ -23,13 +23,11 @@ pub struct OnlineStatusMap {
|
||||
}
|
||||
|
||||
impl OnlineStatusMap {
|
||||
|
||||
/// Creates a new instance with one map entry for each configured
|
||||
/// changer (or 'VirtualTapeDrive', which has an internal
|
||||
/// changer). The map entry is set to 'None' to indicate that we
|
||||
/// do not have information about the online status.
|
||||
pub fn new(config: &SectionConfigData) -> Result<Self, Error> {
|
||||
|
||||
let mut map = HashMap::new();
|
||||
|
||||
let changers: Vec<ScsiTapeChanger> = config.convert_to_typed_array("changer")?;
|
||||
@ -42,7 +40,10 @@ impl OnlineStatusMap {
|
||||
map.insert(vtape.name.clone(), None);
|
||||
}
|
||||
|
||||
Ok(Self { map, changer_map: HashMap::new() })
|
||||
Ok(Self {
|
||||
map,
|
||||
changer_map: HashMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the assiciated changer name for a media.
|
||||
@ -61,11 +62,14 @@ impl OnlineStatusMap {
|
||||
}
|
||||
|
||||
/// Update the online set for the specified changer
|
||||
pub fn update_online_status(&mut self, changer_name: &str, online_set: HashSet<Uuid>) -> Result<(), Error> {
|
||||
|
||||
pub fn update_online_status(
|
||||
&mut self,
|
||||
changer_name: &str,
|
||||
online_set: HashSet<Uuid>,
|
||||
) -> Result<(), Error> {
|
||||
match self.map.get(changer_name) {
|
||||
None => bail!("no such changer '{}' device", changer_name),
|
||||
Some(None) => { /* Ok */ },
|
||||
Some(None) => { /* Ok */ }
|
||||
Some(Some(_)) => {
|
||||
// do not allow updates to keep self.changer_map consistent
|
||||
bail!("update_online_status '{}' called twice", changer_name);
|
||||
@ -73,7 +77,8 @@ impl OnlineStatusMap {
|
||||
}
|
||||
|
||||
for uuid in online_set.iter() {
|
||||
self.changer_map.insert(uuid.clone(), changer_name.to_string());
|
||||
self.changer_map
|
||||
.insert(uuid.clone(), changer_name.to_string());
|
||||
}
|
||||
|
||||
self.map.insert(changer_name.to_string(), Some(online_set));
|
||||
@ -87,7 +92,6 @@ impl OnlineStatusMap {
|
||||
/// Returns a HashSet containing all found media Uuid. This only
|
||||
/// returns media found in Inventory.
|
||||
pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> HashSet<Uuid> {
|
||||
|
||||
let mut online_set = HashSet::new();
|
||||
|
||||
for drive_status in status.drives.iter() {
|
||||
@ -99,7 +103,9 @@ pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> Ha
|
||||
}
|
||||
|
||||
for slot_info in status.slots.iter() {
|
||||
if slot_info.import_export { continue; }
|
||||
if slot_info.import_export {
|
||||
continue;
|
||||
}
|
||||
if let ElementStatus::VolumeTag(ref label_text) = slot_info.status {
|
||||
if let Some(media_id) = inventory.find_media_by_label_text(label_text) {
|
||||
online_set.insert(media_id.label.uuid.clone());
|
||||
@ -113,8 +119,10 @@ pub fn mtx_status_to_online_set(status: &MtxStatus, inventory: &Inventory) -> Ha
|
||||
/// Update online media status
|
||||
///
|
||||
/// For a single 'changer', or else simply ask all changer devices.
|
||||
pub fn update_online_status(state_path: &Path, changer: Option<&str>) -> Result<OnlineStatusMap, Error> {
|
||||
|
||||
pub fn update_online_status(
|
||||
state_path: &Path,
|
||||
changer: Option<&str>,
|
||||
) -> Result<OnlineStatusMap, Error> {
|
||||
let (config, _digest) = pbs_config::drive::config()?;
|
||||
|
||||
let mut inventory = Inventory::load(state_path)?;
|
||||
@ -135,7 +143,10 @@ pub fn update_online_status(state_path: &Path, changer: Option<&str>) -> Result<
|
||||
let status = match changer_config.status(false) {
|
||||
Ok(status) => status,
|
||||
Err(err) => {
|
||||
eprintln!("unable to get changer '{}' status - {}", changer_config.name, err);
|
||||
eprintln!(
|
||||
"unable to get changer '{}' status - {}",
|
||||
changer_config.name, err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@ -172,7 +183,10 @@ pub fn update_online_status(state_path: &Path, changer: Option<&str>) -> Result<
|
||||
|
||||
if let Some(changer) = changer {
|
||||
if !found_changer {
|
||||
bail!("update_online_status failed - no such changer '{}'", changer);
|
||||
bail!(
|
||||
"update_online_status failed - no such changer '{}'",
|
||||
changer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +202,6 @@ pub fn update_changer_online_status(
|
||||
changer_name: &str,
|
||||
label_text_list: &[String],
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut online_map = OnlineStatusMap::new(drive_config)?;
|
||||
let mut online_set = HashSet::new();
|
||||
for label_text in label_text_list.iter() {
|
||||
|
Reference in New Issue
Block a user