diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 46c650ed..5b7486df 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -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.
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.
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."
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index c362335a..83201738 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -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.
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.
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."
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 801ec39a..94fcd7e9 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -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.
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.
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."
diff --git a/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
index 1ed0fa4a..ba752ce4 100644
--- a/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
+++ b/app/MindWork AI Studio/Tools/FileExtractionErrorCode.cs
@@ -23,6 +23,12 @@ public enum FileExtractionErrorCode
INVALID_REQUEST,
FILE_NOT_FOUND,
FILE_NOT_READABLE,
+
+ ///
+ /// Another program holds the file open and denies reading it.
+ ///
+ FILE_LOCKED,
+
FORMAT_DETECTION_FAILED,
NOT_A_VALID_PDF,
NOT_A_VALID_SPREADSHEET,
diff --git a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs
index e1297678..65a47697 100644
--- a/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs
+++ b/app/MindWork AI Studio/Tools/FileExtractionResultExtensions.cs
@@ -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_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.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.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.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_CONTENT => TB("The file '{0}' did not provide any content and was not sent."),
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md
index 950cf288..2c3faccc 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.1.md
@@ -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.
- 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 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 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.
diff --git a/runtime/src/file_data.rs b/runtime/src/file_data.rs
index 64f3c3f9..8ba8db86 100644
--- a/runtime/src/file_data.rs
+++ b/runtime/src/file_data.rs
@@ -93,6 +93,10 @@ pub enum ExtractionErrorCode {
FileNotFound,
FileNotReadable,
+
+ /// Another process holds the file open and denies us reading it.
+ FileLocked,
+
FormatDetectionFailed,
NotAValidPdf,
NotAValidSpreadsheet,
@@ -147,6 +151,40 @@ impl fmt::Display 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)]
pub struct Base64Image {
pub id: String,
@@ -303,8 +341,14 @@ async fn stream_data(file_path: &str, extract_images: bool) -> Result format,
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.
async fn ensure_pdf_header(file_path: &str) -> Result<()> {
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}"),
))?;
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}"),
))?.len();
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(
- ExtractionErrorCode::FileNotReadable,
+ classify_io_error(&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 {
/// away, is not reported as a corrupt workbook.
fn classify_spreadsheet_error_code(error: &CalamineError) -> ExtractionErrorCode {
match error {
- CalamineError::Io(_) => ExtractionErrorCode::FileNotReadable,
+ CalamineError::Io(io_error) => classify_io_error(io_error),
_ => ExtractionErrorCode::NotAValidSpreadsheet,
}
}