1
0
mirror of https://gitlab.com/comunic/comunicapiv3 synced 2024-11-22 13:29:21 +00:00

Add support for multiple files types

This commit is contained in:
Pierre HUBERT 2021-03-06 09:35:36 +01:00
parent dc93d58d6b
commit afcce8463f
11 changed files with 377 additions and 35 deletions

84
Cargo.lock generated
View File

@ -500,7 +500,7 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244" checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244"
dependencies = [ dependencies = [
"num-bigint", "num-bigint 0.2.6",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde", "serde",
@ -640,6 +640,27 @@ dependencies = [
"bytes 1.0.1", "bytes 1.0.1",
] ]
[[package]]
name = "bzip2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.10+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.66" version = "1.0.66"
@ -707,6 +728,8 @@ dependencies = [
"lazy_static", "lazy_static",
"mailchecker", "mailchecker",
"mime_guess", "mime_guess",
"mp3-metadata",
"mp4",
"mysql", "mysql",
"pdf", "pdf",
"percent-encoding", "percent-encoding",
@ -718,6 +741,7 @@ dependencies = [
"sha1", "sha1",
"webrtc-sdp", "webrtc-sdp",
"yaml-rust", "yaml-rust",
"zip",
] ]
[[package]] [[package]]
@ -957,15 +981,15 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.20" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 0.1.10",
"crc32fast", "crc32fast",
"libc", "libc",
"libz-sys", "libz-sys",
"miniz_oxide 0.4.3", "miniz_oxide 0.3.7",
] ]
[[package]] [[package]]
@ -1527,6 +1551,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655"
dependencies = [ dependencies = [
"cc", "cc",
"libc",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -1727,6 +1752,26 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "mp3-metadata"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb969bc3573726b0bf60238d5d70f1aa6cc0f9e87f8db3e047b8309317319699"
[[package]]
name = "mp4"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "999db0cca8fd8c1b2352f362691bdafbd90069efd581144066a7909895d23b86"
dependencies = [
"byteorder",
"bytes 0.5.6",
"num-rational",
"serde",
"serde_json",
"thiserror",
]
[[package]] [[package]]
name = "mutate_once" name = "mutate_once"
version = "0.1.1" version = "0.1.1"
@ -1771,7 +1816,7 @@ dependencies = [
"flate2", "flate2",
"lazy_static", "lazy_static",
"lexical", "lexical",
"num-bigint", "num-bigint 0.2.6",
"num-traits", "num-traits",
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
@ -1855,6 +1900,17 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-bigint"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.44" version = "0.1.44"
@ -1883,8 +1939,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"num-bigint 0.3.1",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
] ]
[[package]] [[package]]
@ -3249,3 +3307,17 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [ dependencies = [
"linked-hash-map", "linked-hash-map",
] ]
[[package]]
name = "zip"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a8977234acab718eb2820494b2f96cbb16004c19dddf88b7445b27381450997"
dependencies = [
"byteorder",
"bzip2",
"crc32fast",
"flate2",
"thiserror",
"time 0.1.44",
]

View File

@ -33,4 +33,7 @@ regex = "1.4.3"
dashmap = "4.0.2" dashmap = "4.0.2"
reqwest = { version = "0.11.1", features = ["json", "blocking"] } reqwest = { version = "0.11.1", features = ["json", "blocking"] }
webrtc-sdp = "0.3.8" webrtc-sdp = "0.3.8"
bcrypt = "0.9.0" bcrypt = "0.9.0"
mp3-metadata = "0.3.3"
mp4 = "0.8.1"
zip = "0.5.10"

View File

@ -39,7 +39,7 @@ database:
password: pierre password: pierre
# If set to true, every requests made on the database will be shown on the terminal # If set to true, every requests made on the database will be shown on the terminal
log-all-queries: true log-all-queries: false
# Video calls configuration # Video calls configuration

View File

@ -3,7 +3,7 @@
//! @author Pierre Hubert //! @author Pierre Hubert
use serde::Serialize; use serde::Serialize;
use crate::constants::{conservation_policy, MIN_SUPPORTED_MOBILE_VERSION, password_policy, MIN_CONVERSATION_MESSAGE_LENGTH, MAX_CONVERSATION_MESSAGE_LENGTH}; use crate::constants::{ALLOWED_CONVERSATION_FILES_TYPES, conservation_policy, CONVERSATION_FILES_MAX_SIZE, MAX_CONVERSATION_MESSAGE_LENGTH, MIN_CONVERSATION_MESSAGE_LENGTH, MIN_SUPPORTED_MOBILE_VERSION, password_policy};
use crate::data::config::conf; use crate::data::config::conf;
#[derive(Serialize)] #[derive(Serialize)]
@ -39,6 +39,8 @@ pub struct ServerConfig {
data_conservation_policy: DataConservationPolicy, data_conservation_policy: DataConservationPolicy,
min_conversation_message_len: usize, min_conversation_message_len: usize,
max_conversation_message_len: usize, max_conversation_message_len: usize,
allowed_conversation_files_type: [&'static str; 17],
conversation_files_max_size: usize,
} }
impl ServerConfig { impl ServerConfig {
@ -52,6 +54,8 @@ impl ServerConfig {
min_conversation_message_len: MIN_CONVERSATION_MESSAGE_LENGTH, min_conversation_message_len: MIN_CONVERSATION_MESSAGE_LENGTH,
max_conversation_message_len: MAX_CONVERSATION_MESSAGE_LENGTH, max_conversation_message_len: MAX_CONVERSATION_MESSAGE_LENGTH,
allowed_conversation_files_type: ALLOWED_CONVERSATION_FILES_TYPES,
conversation_files_max_size: CONVERSATION_FILES_MAX_SIZE,
password_policy: PasswordPolicy { password_policy: PasswordPolicy {
allow_email_in_password: password_policy::ALLOW_EMAIL_IN_PASSWORD, allow_email_in_password: password_policy::ALLOW_EMAIL_IN_PASSWORD,

View File

@ -166,4 +166,29 @@ pub const MIN_SUPPORTED_MOBILE_VERSION: &str = "1.1.1";
/// Minimum message length /// Minimum message length
pub const MIN_CONVERSATION_MESSAGE_LENGTH: usize = 1; pub const MIN_CONVERSATION_MESSAGE_LENGTH: usize = 1;
pub const MAX_CONVERSATION_MESSAGE_LENGTH: usize = 16000; pub const MAX_CONVERSATION_MESSAGE_LENGTH: usize = 16000;
/// Allowed files type in conversations
pub const ALLOWED_CONVERSATION_FILES_TYPES: [&str; 17] = [
"image/png", "image/jpeg", "image/gif", "image/bmp",
"application/pdf",
"audio/mpeg",
"video/mp4",
"application/zip",
// MS Office docs
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
// Open Office docs
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.spreadsheet",
// Source code docs (UTF-8 encoded)
"text/x-csrc", "text/plain", "text/x-c++src"
];
/// File maximum size in conversations (10 Mb)
pub const CONVERSATION_FILES_MAX_SIZE: usize = 10 * 1024 * 1024;

View File

@ -2,16 +2,15 @@
//! //!
//! @author Pierre Hubert //! @author Pierre Hubert
use crate::api_data::conversation_api::ConversationAPI; use crate::api_data::conversation_api::ConversationAPI;
use crate::api_data::conversation_message_api::ConversationMessageAPI; use crate::api_data::conversation_message_api::ConversationMessageAPI;
use crate::api_data::list_unread_conversations_api::UnreadConversationAPI; use crate::api_data::list_unread_conversations_api::UnreadConversationAPI;
use crate::api_data::res_count_unread_conversations::ResultCountUnreadConversations; use crate::api_data::res_count_unread_conversations::ResultCountUnreadConversations;
use crate::api_data::res_create_conversation::ResCreateConversation; use crate::api_data::res_create_conversation::ResCreateConversation;
use crate::api_data::res_find_private_conversations::ResFindPrivateConversations; use crate::api_data::res_find_private_conversations::ResFindPrivateConversations;
use crate::constants::{MAX_CONVERSATION_MESSAGE_LENGTH, MIN_CONVERSATION_MESSAGE_LENGTH}; use crate::constants::{ALLOWED_CONVERSATION_FILES_TYPES, CONVERSATION_FILES_MAX_SIZE, MAX_CONVERSATION_MESSAGE_LENGTH, MIN_CONVERSATION_MESSAGE_LENGTH};
use crate::controllers::user_ws_controller; use crate::controllers::user_ws_controller;
use crate::data::base_request_handler::BaseRequestHandler; use crate::data::base_request_handler::{BaseRequestHandler, RequestValue};
use crate::data::conversation::{ConversationMemberSetting, NewConversationSettings}; use crate::data::conversation::{ConversationMemberSetting, NewConversationSettings};
use crate::data::conversation_message::ConversationMessageFile; use crate::data::conversation_message::ConversationMessageFile;
use crate::data::error::Res; use crate::data::error::Res;
@ -25,6 +24,7 @@ use crate::helpers::{conversations_helper, events_helper, user_helper};
use crate::helpers::events_helper::Event; use crate::helpers::events_helper::Event;
use crate::routes::RequestResult; use crate::routes::RequestResult;
use crate::utils::string_utils::remove_html_nodes; use crate::utils::string_utils::remove_html_nodes;
use crate::utils::user_data_utils::{delete_user_data_file_if_exists, user_data_path};
/// Create a new conversation /// Create a new conversation
pub fn create(r: &mut HttpRequestHandler) -> RequestResult { pub fn create(r: &mut HttpRequestHandler) -> RequestResult {
@ -251,22 +251,100 @@ pub fn send_message(r: &mut HttpRequestHandler) -> RequestResult {
// TODO : add support for other files type // TODO : add support for other files type
// Get image // Get associated file
let file = match r.has_file("image") { let file = match r.post_parameter_opt("file") {
false => None, Some(RequestValue::File(file)) => {
true => {
let path = r.save_post_image("image", "conversations", 1200, 1200)?; // File name
let mut name = file.name.to_string();
if file.buff.len() > CONVERSATION_FILES_MAX_SIZE {
r.bad_request("File is too big!".to_string())?;
}
// Determine file mime type
let mut mime_type = r.post_file_type("file")?;
// Check for thumbnail
let mut thumbnail = match r.has_file("thumbnail") {
false => None,
true => Some("thumbnail".to_string())
};
let path;
if !ALLOWED_CONVERSATION_FILES_TYPES.contains(&mime_type.as_str()) {
r.bad_request("File type is not allowed!".to_string())?;
}
// Images
if mime_type.starts_with("image/") {
if let None = thumbnail {
thumbnail = Some("file".to_string());
}
path = r.save_post_image("file", "conversation", 2000, 2000)?;
mime_type = "image/png".to_string();
name = "picture.png".to_string();
}
// PDF
else if mime_type.eq("application/pdf") {
path = r.save_post_pdf("file", "conversation")?;
}
// MP3
else if mime_type.eq("audio/mpeg") {
path = r.save_post_mp3("file", "conversation")?;
}
// MP4
else if mime_type.eq("video/mp4") {
path = r.save_post_mp4("file", "conversation")?;
}
// ZIP archive
else if mime_type.eq("application/zip") {
path = r.save_post_zip("file", "conversation")?;
}
// Office document
else if mime_type.starts_with("application/") {
path = r.save_post_office_doc("file", "conversation")?;
}
// Text files
else {
path = r.save_post_txt_doc("file", "conversation")?;
}
// Attempt to save thumbnail, if it fails we can not save message
let thumbnail = match thumbnail {
None => None,
Some(f) => Some(match r.save_post_image(&f, "conversations-thumb", 200, 200) {
Ok(s) => Ok(s),
Err(e) => {
eprintln!("Failed to save conversation thumbnail! {:#?}", e);
delete_user_data_file_if_exists(&path).unwrap();
Err(e)
}
}?)
};
Some(ConversationMessageFile { Some(ConversationMessageFile {
path: path.clone(), path: path.clone(),
size: std::fs::metadata(&path)?.len(), size: std::fs::metadata(user_data_path(path.as_ref()))?.len(),
name: "picture.png".to_string(), name,
thumbnail: Some(r.save_post_image("image", "conversations", 50, 50)?), thumbnail,
r#type: "image/png".to_string(), r#type: mime_type,
}) })
} }
_ => None,
}; };
// Get message, if there is no image // Get message, if there is no file
let message = if let None = file { let message = if let None = file {
let msg = r.post_string_without_html("message", MIN_CONVERSATION_MESSAGE_LENGTH, true)?; let msg = r.post_string_without_html("message", MIN_CONVERSATION_MESSAGE_LENGTH, true)?;

View File

@ -3,6 +3,7 @@
//! Base handling code for all user requests //! Base handling code for all user requests
use std::collections::HashSet;
use std::error::Error; use std::error::Error;
use exif::In; use exif::In;
@ -23,11 +24,14 @@ use crate::data::user_token::UserAccessToken;
use crate::helpers::{account_helper, comments_helper, conversations_helper, custom_emojies_helper, friends_helper, groups_helper, posts_helper, user_helper, virtual_directory_helper}; use crate::helpers::{account_helper, comments_helper, conversations_helper, custom_emojies_helper, friends_helper, groups_helper, posts_helper, user_helper, virtual_directory_helper};
use crate::helpers::virtual_directory_helper::VirtualDirType; use crate::helpers::virtual_directory_helper::VirtualDirType;
use crate::routes::RequestResult; use crate::routes::RequestResult;
use crate::utils::mp3_utils::is_valid_mp3;
use crate::utils::mp4_utils::is_valid_mp4;
use crate::utils::pdf_utils::is_valid_pdf; use crate::utils::pdf_utils::is_valid_pdf;
use crate::utils::string_utils::{check_emoji_code, check_string_before_insert, check_url, remove_html_nodes, check_html_color}; use crate::utils::string_utils::{check_emoji_code, check_html_color, check_string_before_insert, check_url, remove_html_nodes};
use crate::utils::user_data_utils::{generate_new_user_data_file_name, prepare_file_creation, user_data_path}; use crate::utils::user_data_utils::{generate_new_user_data_file_name, prepare_file_creation, user_data_path};
use crate::utils::virtual_directories_utils; use crate::utils::virtual_directories_utils;
use std::collections::HashSet; use crate::utils::zip_utils::is_valid_zip;
use std::str::from_utf8;
#[derive(Serialize)] #[derive(Serialize)]
struct SuccessMessage { struct SuccessMessage {
@ -272,6 +276,34 @@ pub trait BaseRequestHandler {
unreachable!(); unreachable!();
} }
/// Get the mime type of a file included in the request
fn post_file_type(&mut self, name: &str) -> Res<String> {
let file = self.post_file(name)?;
let filetype = mime_guess::from_path(&file.name)
.first()
.map(|m| format!("{}/{}", m.type_(), m.subtype()));
if let None = filetype {
self.bad_request(format!("Could not determine file type in '{}' !", name))?;
unreachable!();
}
Ok(filetype.unwrap())
}
/// Get the extension of a file included in the request
fn post_file_ext(&mut self, name: &str, default: &str) -> Res<String> {
let suffix = self.post_file_type(name)?
.parse::<mime_guess::mime::Mime>()?
.suffix()
.map(|s| s.as_str())
.unwrap_or(default)
.to_string();
Ok(suffix)
}
/// Save an image in user data directory /// Save an image in user data directory
fn save_post_image(&mut self, name: &str, folder: &str, max_w: u32, max_h: u32) -> ResultBoxError<String> { fn save_post_image(&mut self, name: &str, folder: &str, max_w: u32, max_h: u32) -> ResultBoxError<String> {
@ -329,8 +361,25 @@ pub trait BaseRequestHandler {
Ok(target_file_path.to_string_lossy().to_string()) Ok(target_file_path.to_string_lossy().to_string())
} }
/// Save a file included in the request
fn save_post_file(&mut self, name: &str, folder: &str, ext: &str) -> Res<String> {
let file = self.post_file(name)?;
// Avoid memory warnings
let copied_buff = file.buff.clone();
// Determine pdf file destination
let target_user_data_folder = prepare_file_creation(self.user_id_ref()?, folder)?;
let target_file_path = generate_new_user_data_file_name(target_user_data_folder.as_path(), ext)?;
let target_sys_path = user_data_path(target_file_path.as_path());
std::fs::write(target_sys_path, &copied_buff.as_ref())?;
Ok(target_file_path.to_string_lossy().to_string())
}
/// Save a pdf included in the request /// Save a pdf included in the request
fn save_post_pdf(&mut self, name: &str, folder: &str) -> ResultBoxError<String> { fn save_post_pdf(&mut self, name: &str, folder: &str) -> Res<String> {
let file = self.post_file(name)?; let file = self.post_file(name)?;
if !is_valid_pdf(&file.buff)? { if !is_valid_pdf(&file.buff)? {
@ -338,17 +387,76 @@ pub trait BaseRequestHandler {
unreachable!(); unreachable!();
} }
// Avoid memory warnings self.save_post_file(name, folder, "pdf")
let copied_buff = file.buff.clone(); }
// Determine pdf file destination /// Save a mp3 file included in the request
let target_user_data_folder = prepare_file_creation(self.user_id_ref()?, folder)?; fn save_post_mp3(&mut self, name: &str, folder: &str) -> Res<String> {
let target_file_path = generate_new_user_data_file_name(target_user_data_folder.as_path(), "pdf")?; let file = self.post_file(name)?;
let target_sys_path = user_data_path(target_file_path.as_path());
std::fs::write(target_sys_path, &copied_buff.as_ref())?; if !is_valid_mp3(&file.buff) {
self.bad_request(format!("Invalid MP3 file specified in {} !", name))?;
unreachable!();
}
Ok(target_file_path.to_string_lossy().to_string()) self.save_post_file(name, folder, "mp3")
}
/// Save a mp4 file included in the request
fn save_post_mp4(&mut self, name: &str, folder: &str) -> Res<String> {
let file = self.post_file(name)?;
if !is_valid_mp4(&file.buff) {
self.bad_request(format!("Invalid MP4 file specified in {} !", name))?;
unreachable!();
}
self.save_post_file(name, folder, "mp4")
}
/// Save a zip file included in the request
fn save_post_zip(&mut self, name: &str, folder: &str) -> Res<String> {
let file = self.post_file(name)?;
if !is_valid_zip(&file.buff) {
self.bad_request(format!("Invalid ZIP archive file specified in {} !", name))?;
unreachable!();
}
self.save_post_file(name, folder, "zip")
}
/// Save an office document included in the request
fn save_post_office_doc(&mut self, name: &str, folder: &str) -> Res<String> {
let file = self.post_file(name)?;
if !is_valid_zip(&file.buff) {
self.bad_request(format!("Invalid ZIP archive file specified in {} !", name))?;
unreachable!();
}
let mime = self.post_file_type(name)?;
if !mime.starts_with("application/") {
self.bad_request(format!("Invalid file name in {} !", name))?;
unreachable!();
}
let ext = &self.post_file_ext(name, "zip")?;
self.save_post_file(name, folder, ext)
}
/// Save a simple text document included in the request
fn save_post_txt_doc(&mut self, name: &str, folder: &str) -> Res<String> {
let file = self.post_file(name)?;
if from_utf8(&file.buff).is_err() {
self.bad_request(format!("File in {} is not UTF-8!", name))?;
unreachable!();
}
let ext = &self.post_file_ext(name, "txt")?;
self.save_post_file(name, folder, ext)
} }
/// Get an integer included in the POST request /// Get an integer included in the POST request

View File

@ -7,4 +7,7 @@ pub mod user_data_utils;
pub mod virtual_directories_utils; pub mod virtual_directories_utils;
pub mod date_utils; pub mod date_utils;
pub mod string_utils; pub mod string_utils;
pub mod pdf_utils; pub mod pdf_utils;
pub mod mp3_utils;
pub mod mp4_utils;
pub mod zip_utils;

16
src/utils/mp3_utils.rs Normal file
View File

@ -0,0 +1,16 @@
//! # MP3 utilities
//!
//! @author Pierre Hubert
/// Check out whether a file is a valid MP3 file or not
pub fn is_valid_mp3(file: &[u8]) -> bool {
let res = mp3_metadata::read_from_slice(file);
match res {
Ok(_) => true,
Err(e) => {
eprintln!("Error while parsing MP3 file ! {:#?}", e);
false
}
}
}

17
src/utils/mp4_utils.rs Normal file
View File

@ -0,0 +1,17 @@
//! # MP utilities
//!
//! @author Pierre Hubert
/// Check out whether an MP4 file is valid or not
pub fn is_valid_mp4(file: &[u8]) -> bool {
let cursor = std::io::Cursor::new(file);
let reader = mp4::Mp4Reader::read_header(cursor, file.len() as u64);
match reader {
Ok(_) => true,
Err(e) => {
eprintln!("Failed to read MP4! {:#?}", e);
false
}
}
}

16
src/utils/zip_utils.rs Normal file
View File

@ -0,0 +1,16 @@
//! # ZIP utilities
//!
//! @author Pierre Hubert
/// Check out whether a given file is a valid ZIP archive or not
pub fn is_valid_zip(file: &[u8]) -> bool {
let cursor = std::io::Cursor::new(file);
match zip::ZipArchive::new(cursor) {
Ok(_) => true,
Err(e) => {
eprintln!("Failed to read ZIP archive! {:#?}", e);
false
}
}
}