Added detection for files that are locked by another program

This commit is contained in:
Thorsten Sommer 2026-08-10 13:13:02 +02:00
parent ca894f894d
commit 2dc8f31b38
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
7 changed files with 67 additions and 7 deletions

View File

@ -9103,6 +9103,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2064321829"]
-- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely. -- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."
-- The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2701144378"] = "The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open."
-- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted. -- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted."

View File

@ -9105,6 +9105,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2064321829"]
-- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely. -- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "Die Datei „{0}“ ist keine lesbare Tabellenkalkulation und wurde nicht gesendet. Möglicherweise ist sie beschädigt oder unvollständig übertragen worden." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "Die Datei „{0}“ ist keine lesbare Tabellenkalkulation und wurde nicht gesendet. Möglicherweise ist sie beschädigt oder unvollständig übertragen worden."
-- The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2701144378"] = "Die Datei „{0}“ ist derzeit in einem anderen Programm geöffnet und wurde daher nicht gesendet. Bitte schließen Sie die Datei und versuchen Sie es erneut. Wenn die Datei auf einem freigegebenen Netzlaufwerk gespeichert ist, könnte sie von einem Kollegen geöffnet sein."
-- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted. -- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Das Lesen der Datei „{0}“ dauerte zu lange und wurde abgebrochen. Daher wurde die Datei nicht gesendet. Wenn die Datei auf einem Netzlaufwerk gespeichert ist, könnte die Verbindung langsam oder unterbrochen sein." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Das Lesen der Datei „{0}“ dauerte zu lange und wurde abgebrochen. Daher wurde die Datei nicht gesendet. Wenn die Datei auf einem Netzlaufwerk gespeichert ist, könnte die Verbindung langsam oder unterbrochen sein."

View File

@ -9105,6 +9105,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2064321829"]
-- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely. -- The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2240855899"] = "The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."
-- The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2701144378"] = "The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open."
-- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted. -- Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted." UI_TEXT_CONTENT["AISTUDIO::TOOLS::FILEEXTRACTIONRESULTEXTENSIONS::T2793077828"] = "Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted."

View File

@ -23,6 +23,12 @@ public enum FileExtractionErrorCode
INVALID_REQUEST, INVALID_REQUEST,
FILE_NOT_FOUND, FILE_NOT_FOUND,
FILE_NOT_READABLE, FILE_NOT_READABLE,
/// <summary>
/// Another program holds the file open and denies reading it.
/// </summary>
FILE_LOCKED,
FORMAT_DETECTION_FAILED, FORMAT_DETECTION_FAILED,
NOT_A_VALID_PDF, NOT_A_VALID_PDF,
NOT_A_VALID_SPREADSHEET, NOT_A_VALID_SPREADSHEET,

View File

@ -52,12 +52,12 @@ internal static class FileExtractionResultExtensions
{ {
FileExtractionErrorCode.FILE_NOT_FOUND => TB("The file '{0}' does not exist anymore and was not sent."), FileExtractionErrorCode.FILE_NOT_FOUND => TB("The file '{0}' does not exist anymore and was not sent."),
FileExtractionErrorCode.FILE_NOT_READABLE => TB("The file '{0}' could not be read and was not sent. When the file is stored on a network drive, the drive might be unavailable, or another program might be blocking the file."), FileExtractionErrorCode.FILE_NOT_READABLE => TB("The file '{0}' could not be read and was not sent. When the file is stored on a network drive, the drive might be unavailable, or another program might be blocking the file."),
FileExtractionErrorCode.FILE_LOCKED => TB("The file '{0}' is currently open in another program, which is why it was not sent. Please close the file and try again. When the file is stored on a shared network drive, a colleague might have it open."),
FileExtractionErrorCode.TIMEOUT => TB("Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted."), FileExtractionErrorCode.TIMEOUT => TB("Reading the file '{0}' took too long and was stopped, so the file was not sent. When the file is stored on a network drive, the connection might be slow or interrupted."),
FileExtractionErrorCode.NOT_A_VALID_PDF => TB("The file '{0}' is not a readable PDF and was not sent. It might be damaged or transferred incompletely."), FileExtractionErrorCode.NOT_A_VALID_PDF => TB("The file '{0}' is not a readable PDF and was not sent. It might be damaged or transferred incompletely."),
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."), FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("The file '{0}' is not a readable spreadsheet and was not sent. It might be damaged or transferred incompletely."),
FileExtractionErrorCode.PDF_ENCRYPTED => TB("The file '{0}' is protected and could not be opened, so it was not sent."), FileExtractionErrorCode.PDF_ENCRYPTED => TB("The file '{0}' is protected and could not be opened, so it was not sent."),
FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("AI Studio was not able to start its PDF engine, so the file '{0}' was not sent."), FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("AI Studio was not able to start its PDF engine, so the file '{0}' was not sent."),
FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Reading the file '{0}' needs Pandoc, which is not available, so the file was not sent."), FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Reading the file '{0}' needs Pandoc, which is not available, so the file was not sent."),
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No text could be read from the file '{0}', so it was not sent. The file might consist of scanned images without a text layer."), FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No text could be read from the file '{0}', so it was not sent. The file might consist of scanned images without a text layer."),
FileExtractionErrorCode.NO_CONTENT => TB("The file '{0}' did not provide any content and was not sent."), FileExtractionErrorCode.NO_CONTENT => TB("The file '{0}' did not provide any content and was not sent."),

View File

@ -11,6 +11,7 @@
- Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does. - Improved reading large files from slow locations such as network drives. AI Studio now waits considerably longer before it gives up, and it tells you when it does.
- Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants. - Changed how approvals for assistant plugins combine when your organization deploys several configurations. They now add up, so a department can approve additional assistant plugins without repeating the approvals of the company-wide configuration. Previously, the last configuration replaced all earlier approvals, which silently required a new security check for those assistants.
- Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file. - Fixed attached files reaching the AI as empty documents when AI Studio could not read them. The AI then answered as if your file had no content, and nothing pointed to a problem. AI Studio now names the cause instead, for example an unavailable network drive, a file another program is blocking, a protected PDF, or a scanned PDF without a text layer, and it no longer attaches such a file.
- Fixed files that are open in another program being reported as an unrecognized file type. AI Studio now tells you that the file is currently open elsewhere and asks you to close it. This also works for files on shared network drives, where a colleague might have the file open.
- Fixed a single unreadable page of a PDF silently cutting off the rest of the document. The remaining pages are now used, and AI Studio tells you which pages are missing. - Fixed a single unreadable page of a PDF silently cutting off the rest of the document. The remaining pages are now used, and AI Studio tells you which pages are missing.
- Fixed a single unreadable sheet of a spreadsheet silently dropping all remaining sheets. - Fixed a single unreadable sheet of a spreadsheet silently dropping all remaining sheets.
- Fixed PDFs, text files, spreadsheets, and presentations requiring Pandoc. Only Word documents, OpenDocument text files, and HTML files need Pandoc, so every other file can now be attached and read without it. - Fixed PDFs, text files, spreadsheets, and presentations requiring Pandoc. Only Word documents, OpenDocument text files, and HTML files need Pandoc, so every other file can now be attached and read without it.

View File

@ -93,6 +93,10 @@ pub enum ExtractionErrorCode {
FileNotFound, FileNotFound,
FileNotReadable, FileNotReadable,
/// Another process holds the file open and denies us reading it.
FileLocked,
FormatDetectionFailed, FormatDetectionFailed,
NotAValidPdf, NotAValidPdf,
NotAValidSpreadsheet, NotAValidSpreadsheet,
@ -147,6 +151,40 @@ impl fmt::Display for ExtractionError {
impl std::error::Error for ExtractionError {} impl std::error::Error for ExtractionError {}
/// Detects whether a file system error means that another process holds the file open.
///
/// Windows answers with `ERROR_SHARING_VIOLATION` (32) or `ERROR_LOCK_VIOLATION` (33). This also
/// covers files on a network drive, because the SMB server enforces the lock and the client
/// surfaces the very same codes.
#[cfg(windows)]
fn is_locked_error(error: &std::io::Error) -> bool {
matches!(error.raw_os_error(), Some(32) | Some(33))
}
/// Detects whether a file system error means that another process holds the file open.
///
/// Unix has no distinct error for this. A lock held through an SMB share surfaces as a permission
/// problem, which we cannot tell apart from an actual permission problem, so we never claim a file
/// is locked here.
#[cfg(not(windows))]
fn is_locked_error(_error: &std::io::Error) -> bool {
false
}
/// Classifies a file system error, so a file which another program holds open is reported as such
/// instead of collapsing into a generic read failure.
fn classify_io_error(error: &std::io::Error) -> ExtractionErrorCode {
if is_locked_error(error) {
return ExtractionErrorCode::FileLocked;
}
match error.kind() {
std::io::ErrorKind::NotFound => ExtractionErrorCode::FileNotFound,
std::io::ErrorKind::InvalidData => ExtractionErrorCode::FormatDetectionFailed,
_ => ExtractionErrorCode::FileNotReadable,
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Base64Image { pub struct Base64Image {
pub id: String, pub id: String,
@ -303,8 +341,14 @@ async fn stream_data(file_path: &str, extract_images: bool) -> Result<ChunkStrea
let fmt = match FileFormat::from_file(&file_path_clone) { let fmt = match FileFormat::from_file(&file_path_clone) {
Ok(format) => format, Ok(format) => format,
Err(error) => { Err(error) => {
error!("Failed to determine file format for '{file_path}': {error}"); //
return Err(ExtractionError::new(ExtractionErrorCode::FormatDetectionFailed, format!("Failed to determine the file format for '{file_path}': {error}")).into()); // Detecting the format opens the file, so this is the first place a file which another
// program holds open fails. Reporting that as a format problem would send the user
// looking in the wrong direction, hence we classify the error instead.
//
let code = classify_io_error(&error);
error!("Failed to read '{file_path}' while determining its file format ({code:?}): {error}");
return Err(ExtractionError::new(code, format!("The file could not be read: {error}")).into());
}, },
}; };
@ -445,18 +489,18 @@ async fn stream_text_file(file_path: &str, use_md_fences: bool, fence_language:
/// the text branch and silently produce empty content. /// the text branch and silently produce empty content.
async fn ensure_pdf_header(file_path: &str) -> Result<()> { async fn ensure_pdf_header(file_path: &str) -> Result<()> {
let file = tokio::fs::File::open(file_path).await.map_err(|error| ExtractionError::new( let file = tokio::fs::File::open(file_path).await.map_err(|error| ExtractionError::new(
ExtractionErrorCode::FileNotReadable, classify_io_error(&error),
format!("The file could not be opened: {error}"), format!("The file could not be opened: {error}"),
))?; ))?;
let file_size = file.metadata().await.map_err(|error| ExtractionError::new( let file_size = file.metadata().await.map_err(|error| ExtractionError::new(
ExtractionErrorCode::FileNotReadable, classify_io_error(&error),
format!("The file size could not be read: {error}"), format!("The file size could not be read: {error}"),
))?.len(); ))?.len();
let mut header = Vec::with_capacity(PDF_HEADER_PROBE_SIZE as usize); let mut header = Vec::with_capacity(PDF_HEADER_PROBE_SIZE as usize);
file.take(PDF_HEADER_PROBE_SIZE).read_to_end(&mut header).await.map_err(|error| ExtractionError::new( file.take(PDF_HEADER_PROBE_SIZE).read_to_end(&mut header).await.map_err(|error| ExtractionError::new(
ExtractionErrorCode::FileNotReadable, classify_io_error(&error),
format!("The first bytes of the file could not be read: {error}"), format!("The first bytes of the file could not be read: {error}"),
))?; ))?;
@ -590,7 +634,7 @@ async fn stream_pdf(file_path: &str) -> Result<ChunkStream> {
/// away, is not reported as a corrupt workbook. /// away, is not reported as a corrupt workbook.
fn classify_spreadsheet_error_code(error: &CalamineError) -> ExtractionErrorCode { fn classify_spreadsheet_error_code(error: &CalamineError) -> ExtractionErrorCode {
match error { match error {
CalamineError::Io(_) => ExtractionErrorCode::FileNotReadable, CalamineError::Io(io_error) => classify_io_error(io_error),
_ => ExtractionErrorCode::NotAValidSpreadsheet, _ => ExtractionErrorCode::NotAValidSpreadsheet,
} }
} }