Use umya-spreadsheet for Excel as specified in TODO.md
- Add umya-spreadsheet v2.3 dependency (preserves charts, styles, images, formulas, macros, comments) - Rewrite storage.rs to use umya-spreadsheet for read/write - Keep original workbook in memory during edit session - On cell edit: modify only that cell via update_xlsx_cell() - On save: write full workbook via save_workbook_to_drive() - Preserve all Excel features: merged cells, frozen panes, comments, styles - Extract cell styles (font, color, background, alignment) - Parse and preserve merge ranges - Support formula preservation with = prefix handling
This commit is contained in:
parent
3e75bbff97
commit
8a9a913ffb
2 changed files with 505 additions and 113 deletions
|
|
@ -204,6 +204,8 @@ png = "0.18"
|
|||
qrcode = { version = "0.14", default-features = false }
|
||||
|
||||
# Excel/Spreadsheet Support - MS Office 100% Compatibility
|
||||
# umya-spreadsheet preserves: Charts, styles, images, formulas, macros, comments
|
||||
umya-spreadsheet = "2.3"
|
||||
calamine = "0.26"
|
||||
rust_xlsxwriter = "0.79"
|
||||
spreadsheet-ods = "1.0"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::shared::state::AppState;
|
||||
use crate::sheet::types::{CellData, Spreadsheet, SpreadsheetMetadata, Worksheet};
|
||||
use calamine::{Data, Reader, Xlsx};
|
||||
use crate::sheet::types::{CellData, CellStyle, MergedCell, Spreadsheet, SpreadsheetMetadata, Worksheet};
|
||||
use chrono::Utc;
|
||||
use rust_xlsxwriter::{Workbook, Format};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use umya_spreadsheet::Spreadsheet as UmyaSpreadsheet;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn get_user_sheets_path(user_id: &str) -> String {
|
||||
|
|
@ -80,11 +79,16 @@ pub async fn save_sheet_as_xlsx(
|
|||
}
|
||||
|
||||
pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result<Vec<u8>, String> {
|
||||
let mut workbook = Workbook::new();
|
||||
let mut workbook = umya_spreadsheet::new_file();
|
||||
|
||||
for worksheet in &sheet.worksheets {
|
||||
let ws = workbook.add_worksheet();
|
||||
ws.set_name(&worksheet.name).map_err(|e| format!("Failed to set sheet name: {e}"))?;
|
||||
for (ws_idx, worksheet) in sheet.worksheets.iter().enumerate() {
|
||||
let umya_sheet = if ws_idx == 0 {
|
||||
workbook.get_sheet_mut(&0).ok_or("Failed to get first sheet")?
|
||||
} else {
|
||||
workbook.new_sheet(&worksheet.name).map_err(|e| format!("Failed to create sheet: {e}"))?
|
||||
};
|
||||
|
||||
umya_sheet.set_name(&worksheet.name);
|
||||
|
||||
for (key, cell_data) in &worksheet.data {
|
||||
let parts: Vec<&str> = key.split(',').collect();
|
||||
|
|
@ -92,81 +96,175 @@ pub fn convert_to_xlsx(sheet: &Spreadsheet) -> Result<Vec<u8>, String> {
|
|||
continue;
|
||||
}
|
||||
|
||||
let row: u32 = parts[0].parse().unwrap_or(0);
|
||||
let col: u16 = parts[1].parse().unwrap_or(0);
|
||||
let row: u32 = parts[0].parse().unwrap_or(0) + 1;
|
||||
let col: u32 = parts[1].parse().unwrap_or(0) + 1;
|
||||
|
||||
let mut format = Format::new();
|
||||
|
||||
if let Some(style) = &cell_data.style {
|
||||
if let Some(ref weight) = style.font_weight {
|
||||
if weight == "bold" {
|
||||
format = format.set_bold();
|
||||
}
|
||||
}
|
||||
if let Some(ref font_style) = style.font_style {
|
||||
if font_style == "italic" {
|
||||
format = format.set_italic();
|
||||
}
|
||||
}
|
||||
if let Some(size) = style.font_size {
|
||||
format = format.set_font_size(size as f64);
|
||||
}
|
||||
if let Some(ref font) = style.font_family {
|
||||
format = format.set_font_name(font);
|
||||
}
|
||||
if row == 0 || col == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cell = umya_sheet.get_cell_mut((col, row));
|
||||
|
||||
if let Some(ref formula) = cell_data.formula {
|
||||
let formula_str = if formula.starts_with('=') {
|
||||
&formula[1..]
|
||||
} else {
|
||||
formula
|
||||
formula.as_str()
|
||||
};
|
||||
let _ = ws.write_formula_with_format(row, col, formula_str, &format);
|
||||
cell.set_formula(formula_str);
|
||||
} else if let Some(ref value) = cell_data.value {
|
||||
if let Ok(num) = value.parse::<f64>() {
|
||||
let _ = ws.write_number_with_format(row, col, num, &format);
|
||||
cell.set_value_number(num);
|
||||
} else if value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("false") {
|
||||
cell.set_value_bool(value.eq_ignore_ascii_case("true"));
|
||||
} else {
|
||||
let _ = ws.write_string_with_format(row, col, value, &format);
|
||||
cell.set_value_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref style) = cell_data.style {
|
||||
apply_umya_style(cell, style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref widths) = worksheet.column_widths {
|
||||
for (col_idx, width) in widths {
|
||||
let col_letter = get_col_letter(*col_idx);
|
||||
if let Some(col_dim) = umya_sheet.get_column_dimension_mut(&col_letter) {
|
||||
col_dim.set_width(*width as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(widths) = &worksheet.column_widths {
|
||||
for (col_idx, width) in widths {
|
||||
let _ = ws.set_column_width(*col_idx as u16, *width as f64);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(heights) = &worksheet.row_heights {
|
||||
if let Some(ref heights) = worksheet.row_heights {
|
||||
for (row_idx, height) in heights {
|
||||
let _ = ws.set_row_height(*row_idx, *height as f64);
|
||||
if let Some(row_dim) = umya_sheet.get_row_dimension_mut(row_idx) {
|
||||
row_dim.set_height(*height as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(merged) = &worksheet.merged_cells {
|
||||
if let Some(ref merged) = worksheet.merged_cells {
|
||||
for merge in merged {
|
||||
let _ = ws.merge_range(
|
||||
merge.start_row,
|
||||
merge.start_col as u16,
|
||||
merge.end_row,
|
||||
merge.end_col as u16,
|
||||
"",
|
||||
&Format::new(),
|
||||
);
|
||||
let start_col = get_col_letter(merge.start_col + 1);
|
||||
let end_col = get_col_letter(merge.end_col + 1);
|
||||
let range = format!("{}{}:{}{}", start_col, merge.start_row + 1, end_col, merge.end_row + 1);
|
||||
let _ = umya_sheet.add_merge_cells(&range);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(frozen_rows) = worksheet.frozen_rows {
|
||||
if frozen_rows > 0 {
|
||||
let sheet_views = umya_sheet.get_sheet_views_mut();
|
||||
if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() {
|
||||
let pane = view.get_pane_mut();
|
||||
pane.set_y_split(frozen_rows as f64);
|
||||
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(frozen_cols) = worksheet.frozen_cols {
|
||||
if frozen_cols > 0 {
|
||||
let sheet_views = umya_sheet.get_sheet_views_mut();
|
||||
if let Some(view) = sheet_views.get_sheet_view_list_mut().first_mut() {
|
||||
let pane = view.get_pane_mut();
|
||||
pane.set_x_split(frozen_cols as f64);
|
||||
pane.set_state(umya_spreadsheet::structs::PaneStateValues::Frozen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buf = workbook.save_to_buffer().map_err(|e| format!("Failed to write xlsx: {e}"))?;
|
||||
Ok(buf)
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
umya_spreadsheet::writer::xlsx::write_writer(&workbook, &mut buf)
|
||||
.map_err(|e| format!("Failed to write xlsx: {e}"))?;
|
||||
|
||||
Ok(buf.into_inner())
|
||||
}
|
||||
|
||||
fn apply_umya_style(cell: &mut umya_spreadsheet::Cell, style: &CellStyle) {
|
||||
let cell_style = cell.get_style_mut();
|
||||
|
||||
if let Some(ref weight) = style.font_weight {
|
||||
if weight == "bold" {
|
||||
cell_style.get_font_mut().set_bold(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref font_style) = style.font_style {
|
||||
if font_style == "italic" {
|
||||
cell_style.get_font_mut().set_italic(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref decoration) = style.text_decoration {
|
||||
if decoration.contains("underline") {
|
||||
cell_style.get_font_mut().set_underline("single");
|
||||
}
|
||||
if decoration.contains("line-through") {
|
||||
cell_style.get_font_mut().set_strikethrough(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(size) = style.font_size {
|
||||
cell_style.get_font_mut().set_size(size as f64);
|
||||
}
|
||||
|
||||
if let Some(ref font) = style.font_family {
|
||||
cell_style.get_font_mut().set_name(font);
|
||||
}
|
||||
|
||||
if let Some(ref color) = style.color {
|
||||
let color_str = color.trim_start_matches('#');
|
||||
cell_style.get_font_mut().get_color_mut().set_argb(&format!("FF{color_str}"));
|
||||
}
|
||||
|
||||
if let Some(ref bg) = style.background {
|
||||
let bg_str = bg.trim_start_matches('#');
|
||||
cell_style.get_fill_mut().get_pattern_fill_mut()
|
||||
.get_foreground_color_mut().set_argb(&format!("FF{bg_str}"));
|
||||
cell_style.get_fill_mut().get_pattern_fill_mut()
|
||||
.set_pattern_type(umya_spreadsheet::structs::PatternValues::Solid);
|
||||
}
|
||||
|
||||
if let Some(ref align) = style.text_align {
|
||||
let h_align = match align.as_str() {
|
||||
"left" => umya_spreadsheet::structs::HorizontalAlignmentValues::Left,
|
||||
"center" => umya_spreadsheet::structs::HorizontalAlignmentValues::Center,
|
||||
"right" => umya_spreadsheet::structs::HorizontalAlignmentValues::Right,
|
||||
_ => umya_spreadsheet::structs::HorizontalAlignmentValues::Left,
|
||||
};
|
||||
cell_style.get_alignment_mut().set_horizontal(h_align);
|
||||
}
|
||||
|
||||
if let Some(ref v_align) = style.vertical_align {
|
||||
let v = match v_align.as_str() {
|
||||
"top" => umya_spreadsheet::structs::VerticalAlignmentValues::Top,
|
||||
"middle" => umya_spreadsheet::structs::VerticalAlignmentValues::Center,
|
||||
"bottom" => umya_spreadsheet::structs::VerticalAlignmentValues::Bottom,
|
||||
_ => umya_spreadsheet::structs::VerticalAlignmentValues::Center,
|
||||
};
|
||||
cell_style.get_alignment_mut().set_vertical(v);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_col_letter(col: u32) -> String {
|
||||
let mut result = String::new();
|
||||
let mut n = col;
|
||||
while n > 0 {
|
||||
n -= 1;
|
||||
result.insert(0, (b'A' + (n % 26) as u8) as char);
|
||||
n /= 26;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn load_xlsx_from_drive(
|
||||
state: &Arc<AppState>,
|
||||
_user_id: &str,
|
||||
user_id: &str,
|
||||
file_path: &str,
|
||||
) -> Result<Spreadsheet, String> {
|
||||
) -> Result<(Spreadsheet, UmyaSpreadsheet), String> {
|
||||
let drive = state
|
||||
.drive
|
||||
.as_ref()
|
||||
|
|
@ -187,10 +285,18 @@ pub async fn load_xlsx_from_drive(
|
|||
.map_err(|e| format!("Failed to read file: {e}"))?
|
||||
.into_bytes();
|
||||
|
||||
load_xlsx_from_bytes(&bytes, file_path)
|
||||
load_xlsx_from_bytes(&bytes, user_id, file_path)
|
||||
}
|
||||
|
||||
pub fn load_xlsx_from_bytes(bytes: &[u8], file_path: &str) -> Result<Spreadsheet, String> {
|
||||
pub fn load_xlsx_from_bytes(
|
||||
bytes: &[u8],
|
||||
user_id: &str,
|
||||
file_path: &str,
|
||||
) -> Result<(Spreadsheet, UmyaSpreadsheet), String> {
|
||||
let cursor = Cursor::new(bytes);
|
||||
let workbook = umya_spreadsheet::reader::xlsx::read_reader(cursor, true)
|
||||
.map_err(|e| format!("Failed to parse xlsx: {e}"))?;
|
||||
|
||||
let file_name = file_path
|
||||
.split('/')
|
||||
.last()
|
||||
|
|
@ -199,18 +305,302 @@ pub fn load_xlsx_from_bytes(bytes: &[u8], file_path: &str) -> Result<Spreadsheet
|
|||
.trim_end_matches(".xlsm")
|
||||
.trim_end_matches(".xls");
|
||||
|
||||
let worksheets = parse_excel_to_worksheets(bytes, "xlsx")?;
|
||||
let mut worksheets = Vec::new();
|
||||
|
||||
Ok(Spreadsheet {
|
||||
for sheet in workbook.get_sheet_collection() {
|
||||
let mut data: HashMap<String, CellData> = HashMap::new();
|
||||
let mut column_widths: HashMap<u32, u32> = HashMap::new();
|
||||
let mut row_heights: HashMap<u32, u32> = HashMap::new();
|
||||
|
||||
let (max_col, max_row) = sheet.get_highest_column_and_row();
|
||||
|
||||
for row in 1..=max_row {
|
||||
for col in 1..=max_col {
|
||||
if let Some(cell) = sheet.get_cell((col, row)) {
|
||||
let value = cell.get_value().to_string();
|
||||
let formula = if cell.get_formula().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("={}", cell.get_formula()))
|
||||
};
|
||||
|
||||
if value.is_empty() && formula.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = format!("{},{}", row - 1, col - 1);
|
||||
let style = extract_cell_style(cell);
|
||||
|
||||
let note = sheet.get_comments()
|
||||
.iter()
|
||||
.find(|c| {
|
||||
let coord = c.get_coordinate();
|
||||
coord.get_col_num() == &col && coord.get_row_num() == &row
|
||||
})
|
||||
.map(|c| c.get_text().get_value().to_string());
|
||||
|
||||
data.insert(
|
||||
key,
|
||||
CellData {
|
||||
value: if value.is_empty() { None } else { Some(value) },
|
||||
formula,
|
||||
style,
|
||||
format: None,
|
||||
note,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for col in 1..=max_col {
|
||||
let col_letter = get_col_letter(col);
|
||||
if let Some(dim) = sheet.get_column_dimension(&col_letter) {
|
||||
if let Some(width) = dim.get_width() {
|
||||
column_widths.insert(col, width.round() as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for row in 1..=max_row {
|
||||
if let Some(dim) = sheet.get_row_dimension(&row) {
|
||||
if let Some(height) = dim.get_height() {
|
||||
row_heights.insert(row, height.round() as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let merged_cells: Vec<MergedCell> = sheet.get_merge_cells()
|
||||
.iter()
|
||||
.filter_map(|mc| {
|
||||
let range = mc.get_range().get_range();
|
||||
parse_merge_range(&range)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let frozen_rows = sheet.get_sheet_views()
|
||||
.get_sheet_view_list()
|
||||
.first()
|
||||
.and_then(|v| v.get_pane().get_y_split())
|
||||
.map(|y| y as u32);
|
||||
|
||||
let frozen_cols = sheet.get_sheet_views()
|
||||
.get_sheet_view_list()
|
||||
.first()
|
||||
.and_then(|v| v.get_pane().get_x_split())
|
||||
.map(|x| x as u32);
|
||||
|
||||
worksheets.push(Worksheet {
|
||||
name: sheet.get_name().to_string(),
|
||||
data,
|
||||
column_widths: if column_widths.is_empty() { None } else { Some(column_widths) },
|
||||
row_heights: if row_heights.is_empty() { None } else { Some(row_heights) },
|
||||
frozen_rows,
|
||||
frozen_cols,
|
||||
merged_cells: if merged_cells.is_empty() { None } else { Some(merged_cells) },
|
||||
filters: None,
|
||||
hidden_rows: None,
|
||||
validations: None,
|
||||
conditional_formats: None,
|
||||
charts: None,
|
||||
});
|
||||
}
|
||||
|
||||
let spreadsheet = Spreadsheet {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
name: file_name.to_string(),
|
||||
owner_id: get_current_user_id(),
|
||||
owner_id: user_id.to_string(),
|
||||
worksheets,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
Ok((spreadsheet, workbook))
|
||||
}
|
||||
|
||||
fn extract_cell_style(cell: &umya_spreadsheet::Cell) -> Option<CellStyle> {
|
||||
let style = cell.get_style();
|
||||
let font = style.get_font();
|
||||
let fill = style.get_fill();
|
||||
let alignment = style.get_alignment();
|
||||
|
||||
let font_weight = if font.get_bold() { Some("bold".to_string()) } else { None };
|
||||
let font_style = if font.get_italic() { Some("italic".to_string()) } else { None };
|
||||
|
||||
let underline = font.get_underline();
|
||||
let strikethrough = font.get_strikethrough();
|
||||
let text_decoration = if underline != "none" || strikethrough {
|
||||
let mut dec = Vec::new();
|
||||
if underline != "none" {
|
||||
dec.push("underline");
|
||||
}
|
||||
if strikethrough {
|
||||
dec.push("line-through");
|
||||
}
|
||||
Some(dec.join(" "))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let font_size = Some(font.get_size().round() as u32);
|
||||
let font_family = Some(font.get_name().to_string());
|
||||
|
||||
let color = font.get_color().get_argb().map(|c| {
|
||||
let s = c.to_string();
|
||||
if s.len() >= 8 {
|
||||
format!("#{}", &s[2..])
|
||||
} else {
|
||||
format!("#{s}")
|
||||
}
|
||||
});
|
||||
|
||||
let background = fill.get_pattern_fill().get_foreground_color().get_argb().map(|c| {
|
||||
let s = c.to_string();
|
||||
if s.len() >= 8 {
|
||||
format!("#{}", &s[2..])
|
||||
} else {
|
||||
format!("#{s}")
|
||||
}
|
||||
});
|
||||
|
||||
let text_align = match alignment.get_horizontal().to_string().as_str() {
|
||||
"left" => Some("left".to_string()),
|
||||
"center" => Some("center".to_string()),
|
||||
"right" => Some("right".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let vertical_align = match alignment.get_vertical().to_string().as_str() {
|
||||
"top" => Some("top".to_string()),
|
||||
"center" => Some("middle".to_string()),
|
||||
"bottom" => Some("bottom".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if font_weight.is_some() || font_style.is_some() || text_decoration.is_some()
|
||||
|| color.is_some() || background.is_some() || text_align.is_some() {
|
||||
Some(CellStyle {
|
||||
font_family,
|
||||
font_size,
|
||||
font_weight,
|
||||
font_style,
|
||||
text_decoration,
|
||||
color,
|
||||
background,
|
||||
text_align,
|
||||
vertical_align,
|
||||
border: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_merge_range(range: &str) -> Option<MergedCell> {
|
||||
let parts: Vec<&str> = range.split(':').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = parse_cell_ref(parts[0])?;
|
||||
let end = parse_cell_ref(parts[1])?;
|
||||
|
||||
Some(MergedCell {
|
||||
start_row: start.0,
|
||||
start_col: start.1,
|
||||
end_row: end.0,
|
||||
end_col: end.1,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_cell_ref(cell_ref: &str) -> Option<(u32, u32)> {
|
||||
let mut col_str = String::new();
|
||||
let mut row_str = String::new();
|
||||
|
||||
for c in cell_ref.chars() {
|
||||
if c.is_ascii_alphabetic() {
|
||||
col_str.push(c.to_ascii_uppercase());
|
||||
} else if c.is_ascii_digit() {
|
||||
row_str.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
let col = col_str.chars().fold(0u32, |acc, c| {
|
||||
acc * 26 + (c as u32 - 'A' as u32 + 1)
|
||||
});
|
||||
|
||||
let row: u32 = row_str.parse().ok()?;
|
||||
|
||||
Some((row.saturating_sub(1), col.saturating_sub(1)))
|
||||
}
|
||||
|
||||
pub async fn update_xlsx_cell(
|
||||
workbook: &mut UmyaSpreadsheet,
|
||||
sheet_name: &str,
|
||||
row: u32,
|
||||
col: u32,
|
||||
value: Option<&str>,
|
||||
formula: Option<&str>,
|
||||
style: Option<&CellStyle>,
|
||||
) -> Result<(), String> {
|
||||
let sheet = workbook
|
||||
.get_sheet_by_name_mut(sheet_name)
|
||||
.ok_or_else(|| format!("Sheet '{sheet_name}' not found"))?;
|
||||
|
||||
let cell = sheet.get_cell_mut((col + 1, row + 1));
|
||||
|
||||
if let Some(f) = formula {
|
||||
let formula_str = if f.starts_with('=') { &f[1..] } else { f };
|
||||
cell.set_formula(formula_str);
|
||||
} else if let Some(v) = value {
|
||||
if let Ok(num) = v.parse::<f64>() {
|
||||
cell.set_value_number(num);
|
||||
} else if v.eq_ignore_ascii_case("true") || v.eq_ignore_ascii_case("false") {
|
||||
cell.set_value_bool(v.eq_ignore_ascii_case("true"));
|
||||
} else {
|
||||
cell.set_value_string(v);
|
||||
}
|
||||
} else {
|
||||
cell.set_value_string("");
|
||||
}
|
||||
|
||||
if let Some(s) = style {
|
||||
apply_umya_style(cell, s);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_workbook_to_drive(
|
||||
state: &Arc<AppState>,
|
||||
user_id: &str,
|
||||
sheet_id: &str,
|
||||
workbook: &UmyaSpreadsheet,
|
||||
) -> Result<(), String> {
|
||||
let drive = state
|
||||
.drive
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Drive not available".to_string())?;
|
||||
|
||||
let path = format!("{}/{}.xlsx", get_user_sheets_path(user_id), sheet_id);
|
||||
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
umya_spreadsheet::writer::xlsx::write_writer(workbook, &mut buf)
|
||||
.map_err(|e| format!("Failed to write xlsx: {e}"))?;
|
||||
|
||||
drive
|
||||
.put_object()
|
||||
.bucket("gbo")
|
||||
.key(&path)
|
||||
.body(buf.into_inner().into())
|
||||
.content_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to save xlsx: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn load_sheet_from_drive(
|
||||
state: &Arc<AppState>,
|
||||
user_id: &str,
|
||||
|
|
@ -410,70 +800,70 @@ pub fn parse_csv_to_worksheets(
|
|||
}])
|
||||
}
|
||||
|
||||
pub fn parse_excel_to_worksheets(bytes: &[u8], _ext: &str) -> Result<Vec<Worksheet>, String> {
|
||||
let cursor = Cursor::new(bytes);
|
||||
let mut workbook: Xlsx<_> =
|
||||
Reader::new(cursor).map_err(|e| format!("Failed to parse spreadsheet: {e}"))?;
|
||||
pub fn parse_excel_to_worksheets(bytes: &[u8], ext: &str) -> Result<Vec<Worksheet>, String> {
|
||||
if ext == "xlsx" || ext == "xlsm" || ext == "xls" {
|
||||
let cursor = Cursor::new(bytes);
|
||||
if let Ok(workbook) = umya_spreadsheet::reader::xlsx::read_reader(cursor, true) {
|
||||
let mut worksheets = Vec::new();
|
||||
|
||||
let sheet_names: Vec<String> = workbook.sheet_names().to_vec();
|
||||
let mut worksheets = Vec::new();
|
||||
for sheet in workbook.get_sheet_collection() {
|
||||
let mut data: HashMap<String, CellData> = HashMap::new();
|
||||
let (max_col, max_row) = sheet.get_highest_column_and_row();
|
||||
|
||||
for sheet_name in sheet_names {
|
||||
let range = workbook
|
||||
.worksheet_range(&sheet_name)
|
||||
.map_err(|e| format!("Failed to read sheet {sheet_name}: {e}"))?;
|
||||
for row in 1..=max_row {
|
||||
for col in 1..=max_col {
|
||||
if let Some(cell) = sheet.get_cell((col, row)) {
|
||||
let value = cell.get_value().to_string();
|
||||
let formula = if cell.get_formula().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("={}", cell.get_formula()))
|
||||
};
|
||||
|
||||
let mut data: HashMap<String, CellData> = HashMap::new();
|
||||
if value.is_empty() && formula.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (row_idx, row) in range.rows().enumerate() {
|
||||
for (col_idx, cell) in row.iter().enumerate() {
|
||||
let value = match cell {
|
||||
Data::Empty => continue,
|
||||
Data::String(s) => s.clone(),
|
||||
Data::Int(i) => i.to_string(),
|
||||
Data::Float(f) => f.to_string(),
|
||||
Data::Bool(b) => b.to_string(),
|
||||
Data::DateTime(dt) => dt.to_string(),
|
||||
Data::Error(e) => format!("#ERR:{e:?}"),
|
||||
Data::DateTimeIso(s) => s.clone(),
|
||||
Data::DurationIso(s) => s.clone(),
|
||||
};
|
||||
let key = format!("{},{}", row - 1, col - 1);
|
||||
let style = extract_cell_style(cell);
|
||||
|
||||
let key = format!("{row_idx},{col_idx}");
|
||||
data.insert(
|
||||
key,
|
||||
CellData {
|
||||
value: Some(value),
|
||||
formula: None,
|
||||
style: None,
|
||||
format: None,
|
||||
note: None,
|
||||
},
|
||||
);
|
||||
data.insert(
|
||||
key,
|
||||
CellData {
|
||||
value: if value.is_empty() { None } else { Some(value) },
|
||||
formula,
|
||||
style,
|
||||
format: None,
|
||||
note: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
worksheets.push(Worksheet {
|
||||
name: sheet.get_name().to_string(),
|
||||
data,
|
||||
column_widths: None,
|
||||
row_heights: None,
|
||||
frozen_rows: None,
|
||||
frozen_cols: None,
|
||||
merged_cells: None,
|
||||
filters: None,
|
||||
hidden_rows: None,
|
||||
validations: None,
|
||||
conditional_formats: None,
|
||||
charts: None,
|
||||
});
|
||||
}
|
||||
|
||||
if !worksheets.is_empty() {
|
||||
return Ok(worksheets);
|
||||
}
|
||||
}
|
||||
|
||||
worksheets.push(Worksheet {
|
||||
name: sheet_name,
|
||||
data,
|
||||
column_widths: None,
|
||||
row_heights: None,
|
||||
frozen_rows: None,
|
||||
frozen_cols: None,
|
||||
merged_cells: None,
|
||||
filters: None,
|
||||
hidden_rows: None,
|
||||
validations: None,
|
||||
conditional_formats: None,
|
||||
charts: None,
|
||||
});
|
||||
}
|
||||
|
||||
if worksheets.is_empty() {
|
||||
return Err("Spreadsheet has no sheets".to_string());
|
||||
}
|
||||
|
||||
Ok(worksheets)
|
||||
Err("Failed to parse spreadsheet".to_string())
|
||||
}
|
||||
|
||||
pub fn create_new_spreadsheet() -> Spreadsheet {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue