diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..4b53fa3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,19 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: 10.0.x
+ - run: dotnet test MarkItDownWeb.slnx --configuration Release
diff --git a/.gitignore b/.gitignore
index f14a154..f3970af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@ Storage/
.vs/
*.user
*.db
+*.db-*
publish/
+.venv/
diff --git a/MarkItDownWeb.slnx b/MarkItDownWeb.slnx
index d0e6c14..9a37ff9 100644
--- a/MarkItDownWeb.slnx
+++ b/MarkItDownWeb.slnx
@@ -12,4 +12,7 @@
+
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5547eef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+# FileToMarkdown
+
+Aplicación web local para convertir documentos a Markdown. Selecciona un archivo, revisa el resultado en el navegador y descarga un `.md` con el nombre del documento original.
+
+## Funciones
+
+- Interfaz web responsive con vista previa, copia y descarga del Markdown.
+- Historial local de conversiones con búsqueda y reapertura de resultados.
+- Conversión de PDF, DOCX, PPTX, XLSX, HTML y CSV mediante [Microsoft MarkItDown](https://github.com/microsoft/markitdown).
+- Base de datos SQLite y archivos de resultado guardados en el equipo.
+- Validación de extensión, tamaño máximo de 100 MiB y firma de archivos PDF/Office.
+
+La aplicación se ejecuta en tu equipo. Los archivos se envían al proceso web local y a MarkItDown; esta configuración no proporciona autenticación, así que mantenla accesible solo desde tu propio equipo y no la expongas a una red pública.
+
+## Requisitos
+
+- [.NET SDK 10](https://dotnet.microsoft.com/download/dotnet/10.0)
+- Python y `pip`
+
+## Ejecutar en local
+
+Desde la raíz del repositorio:
+
+```bash
+python -m venv .venv
+```
+
+Activa el entorno virtual y luego instala MarkItDown:
+
+```bash
+# Windows PowerShell
+.venv\Scripts\Activate.ps1
+python -m pip install --upgrade pip
+python -m pip install 'markitdown[all]'
+```
+
+```bash
+# Linux / macOS
+source .venv/bin/activate
+python -m pip install --upgrade pip
+python -m pip install 'markitdown[all]'
+```
+
+Inicia la interfaz:
+
+```bash
+dotnet run --project src/Web/MarkItDownWeb.Web.csproj --launch-profile http
+```
+
+Abre [http://localhost:5241](http://localhost:5241). La ruta del ejecutable de Python se puede configurar con `MarkItDown__PythonPath`; por defecto se usa `python` del entorno activo. Por ejemplo, en PowerShell:
+
+```powershell
+$env:MarkItDown__PythonPath = (Get-Command python).Source
+dotnet run --project src/Web/MarkItDownWeb.Web.csproj --launch-profile http
+```
+
+## Comprobar cambios
+
+```bash
+dotnet test MarkItDownWeb.slnx --configuration Release
+```
+
+GitHub Actions ejecuta la misma comprobación al abrir o actualizar un pull request y al hacer push a `master`.
+
+## Uso
+
+1. Elige un PDF, DOCX, PPTX, XLSX, HTML o CSV de hasta 100 MiB.
+2. Pulsa **Convertir a Markdown** y revisa el contenido generado.
+3. Cópialo o descárgalo como `.md`.
+4. Abre una conversión anterior desde el historial. Los datos permanecen en el equipo.
+
+## Datos locales
+
+La base de datos SQLite se crea automáticamente. Los archivos temporales se eliminan al terminar la conversión; el historial conserva los resultados Markdown y los metadatos. Las carpetas se pueden cambiar desde `src/Web/appsettings.json` (`Storage:UploadsFolder`, `Storage:MarkdownFolder` y `ConnectionStrings:DefaultConnection`). No se conserva el documento original después de convertirlo.
+
+## Estructura
+
+```text
+src/
+ Domain/ Entidades y tipos de conversión
+ Application/ Flujo de conversión e interfaces
+ Infrastructure/ MarkItDown, almacenamiento y SQLite
+ Web/ Interfaz ASP.NET Core MVC
+```
+
+## Limitaciones conocidas
+
+- La calidad del Markdown depende del formato y del contenido del documento original.
+- MarkItDown puede necesitar dependencias adicionales para ciertos formatos; consulta su documentación si la instalación completa no está disponible en tu plataforma.
+- Esta versión está pensada para uso local individual; no incluye cuentas, aislamiento entre usuarios ni almacenamiento en la nube.
diff --git a/azure/deploy.sh b/azure/deploy.sh
deleted file mode 100644
index 65e47e7..0000000
--- a/azure/deploy.sh
+++ /dev/null
@@ -1,44 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-# === Azure Deployment Script for MarkItDownWeb ===
-# Adjust variables below before running.
-
-RESOURCE_GROUP="rg-markitdownweb"
-LOCATION="eastus"
-APP_SERVICE_PLAN="plan-markitdownweb"
-WEB_APP_NAME="markitdownweb-app"
-SQL_SERVER="sql-markitdownweb"
-SQL_DB="MarkItDownWeb"
-SQL_ADMIN="sqladmin"
-SQL_PASSWORD="YourStrongPassword123!"
-
-az login
-
-# Resource group
-az group create --name "$RESOURCE_GROUP" --location "$LOCATION"
-
-# App Service Plan + Web App
-az appservice plan create --name "$APP_SERVICE_PLAN" --resource-group "$RESOURCE_GROUP" --sku B1 --is-linux
-az webapp create --resource-group "$RESOURCE_GROUP" --plan "$APP_SERVICE_PLAN" \
- --name "$WEB_APP_NAME" --runtime "DOTNETCORE:9.0"
-
-# SQL Server + Database
-az sql server create --name "$SQL_SERVER" --resource-group "$RESOURCE_GROUP" \
- --admin-user "$SQL_ADMIN" --admin-password "$SQL_PASSWORD"
-az sql db create --resource-group "$RESOURCE_GROUP" --server "$SQL_SERVER" \
- --name "$SQL_DB" --service-objective S0
-
-# Connection string
-CS="Server=tcp:$SQL_SERVER.database.windows.net,1433;Database=$SQL_DB;User ID=$SQL_ADMIN;Password=$SQL_PASSWORD;Trusted_Connection=False;Encrypt=True;"
-az webapp config connection-string set --resource-group "$RESOURCE_GROUP" \
- --name "$WEB_APP_NAME" --settings DefaultConnection="$CS" --connection-string-type SQLAzure
-
-# App settings
-az webapp config appsettings set --resource-group "$RESOURCE_GROUP" --name "$WEB_APP_NAME" \
- --settings MarkItDown__PythonPath="/usr/bin/python3" Storage__UploadsFolder="/home/site/wwwroot/Uploads"
-
-# Publish and deploy
-dotnet publish src/Web -c Release -o ./publish
-cd publish && zip -r ../publish.zip . && cd ..
-az webapp deploy --resource-group "$RESOURCE_GROUP" --name "$WEB_APP_NAME" --src-path publish.zip --type zip
diff --git a/src/Infrastructure/MarkItDownWeb.Infrastructure.csproj b/src/Infrastructure/MarkItDownWeb.Infrastructure.csproj
index 52d741c..b122e7f 100644
--- a/src/Infrastructure/MarkItDownWeb.Infrastructure.csproj
+++ b/src/Infrastructure/MarkItDownWeb.Infrastructure.csproj
@@ -6,11 +6,11 @@
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
-
+
diff --git a/src/Infrastructure/Services/MarkItDownService.cs b/src/Infrastructure/Services/MarkItDownService.cs
index 2a58b2d..393ca1b 100644
--- a/src/Infrastructure/Services/MarkItDownService.cs
+++ b/src/Infrastructure/Services/MarkItDownService.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Text;
using MarkItDownWeb.Application.Interfaces;
+using MarkItDownWeb.Infrastructure.Security;
using Microsoft.Extensions.Configuration;
namespace MarkItDownWeb.Infrastructure.Services;
@@ -19,6 +20,10 @@ public async Task ConvertFileAsync(string absoluteFilePath, Cancellation
if (!File.Exists(absoluteFilePath))
throw new FileNotFoundException("File not found", absoluteFilePath);
+ var extension = Path.GetExtension(absoluteFilePath);
+ if (!FileSignatureValidator.IsValid(absoluteFilePath, extension))
+ throw new InvalidOperationException("The file contents do not match its extension.");
+
var psi = new ProcessStartInfo
{
FileName = _pythonPath,
@@ -41,7 +46,17 @@ public async Task ConvertFileAsync(string absoluteFilePath, Cancellation
process.BeginOutputReadLine();
process.BeginErrorReadLine();
- await process.WaitForExitAsync(ct);
+ try
+ {
+ await process.WaitForExitAsync(ct);
+ }
+ catch (OperationCanceledException)
+ {
+ if (!process.HasExited)
+ process.Kill(entireProcessTree: true);
+
+ throw;
+ }
if (process.ExitCode != 0)
throw new InvalidOperationException($"MarkItDown failed: {stderr}");
diff --git a/src/Web/Controllers/ConversionController.cs b/src/Web/Controllers/ConversionController.cs
index a9b7176..a25a66f 100644
--- a/src/Web/Controllers/ConversionController.cs
+++ b/src/Web/Controllers/ConversionController.cs
@@ -15,16 +15,23 @@ public ConversionController(ConversionService conversionService)
public IActionResult Index() => View(new ConversionViewModel());
[HttpPost]
- [RequestSizeLimit(100_000_000)]
- public async Task Convert(IFormFile file, CancellationToken ct)
+ [RequestSizeLimit(105_906_176)]
+ public async Task Convert(IFormFile? file, CancellationToken ct)
{
if (file is null || file.Length == 0)
return BadRequest(new { error = "No file was received." });
- await using var stream = file.OpenReadStream();
- var result = await _conversionService.ConvertAsync(stream, file.FileName, file.Length, ct);
+ try
+ {
+ await using var stream = file.OpenReadStream();
+ var result = await _conversionService.ConvertAsync(stream, file.FileName, file.Length, ct);
- return Json(result);
+ return Json(result);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { error = ex.Message });
+ }
}
[HttpGet]
@@ -37,8 +44,15 @@ public async Task History(string? search, DateTime? date, Cancell
[HttpGet]
public async Task Item(Guid id, CancellationToken ct)
{
- var result = await _conversionService.GetHistoryItemAsync(id, ct);
- return Json(result);
+ try
+ {
+ var result = await _conversionService.GetHistoryItemAsync(id, ct);
+ return Json(result);
+ }
+ catch (KeyNotFoundException)
+ {
+ return NotFound(new { error = "Conversion not found." });
+ }
}
[HttpPost]
diff --git a/src/Web/MarkItDownWeb.Web.csproj b/src/Web/MarkItDownWeb.Web.csproj
index bd94e24..88a8df8 100644
--- a/src/Web/MarkItDownWeb.Web.csproj
+++ b/src/Web/MarkItDownWeb.Web.csproj
@@ -6,7 +6,7 @@
-
+ runtime; build; native; contentfiles; analyzers; buildtransitiveall
diff --git a/src/Web/Views/Conversion/Index.cshtml b/src/Web/Views/Conversion/Index.cshtml
index 2e95f01..54e1731 100644
--- a/src/Web/Views/Conversion/Index.cshtml
+++ b/src/Web/Views/Conversion/Index.cshtml
@@ -3,39 +3,58 @@
ViewData["Title"] = "Convertir archivos a Markdown";
}
-
-
-
-
Convertir archivos a Markdown
+
+
+
CONVERSIÓN LOCAL
+
Convierte tus documentos a Markdown
+
Sube un archivo, revisa el resultado y descarga tu .md.
+
-
-
-
-
+
+
+
+
+
+
PDF, Word, PowerPoint, Excel, HTML o CSV · máximo 100 MB