Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ Storage/
.vs/
*.user
*.db
*.db-*
publish/
.venv/
3 changes: 3 additions & 0 deletions MarkItDownWeb.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
<Folder Name="/src/Web/">
<Project Path="src/Web/MarkItDownWeb.Web.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MarkItDownWeb.Tests/MarkItDownWeb.Tests.csproj" />
</Folder>
</Solution>
90 changes: 90 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<nombre-del-documento>.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.
44 changes: 0 additions & 44 deletions azure/deploy.sh

This file was deleted.

4 changes: 2 additions & 2 deletions src/Infrastructure/MarkItDownWeb.Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />

</ItemGroup>

Expand Down
17 changes: 16 additions & 1 deletion src/Infrastructure/Services/MarkItDownService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,6 +20,10 @@ public async Task<string> 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,
Expand All @@ -41,7 +46,17 @@ public async Task<string> 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}");
Expand Down
28 changes: 21 additions & 7 deletions src/Web/Controllers/ConversionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@ public ConversionController(ConversionService conversionService)
public IActionResult Index() => View(new ConversionViewModel());

[HttpPost]
[RequestSizeLimit(100_000_000)]
public async Task<IActionResult> Convert(IFormFile file, CancellationToken ct)
[RequestSizeLimit(105_906_176)]
public async Task<IActionResult> 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]
Expand All @@ -37,8 +44,15 @@ public async Task<IActionResult> History(string? search, DateTime? date, Cancell
[HttpGet]
public async Task<IActionResult> 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]
Expand Down
2 changes: 1 addition & 1 deletion src/Web/MarkItDownWeb.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
65 changes: 42 additions & 23 deletions src/Web/Views/Conversion/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,58 @@
ViewData["Title"] = "Convertir archivos a Markdown";
}

<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-9">
<h3 class="mb-3">Convertir archivos a Markdown</h3>
<div class="conversion-shell">
<header class="page-heading mb-4">
<p class="eyebrow mb-2">CONVERSIÓN LOCAL</p>
<h1>Convierte tus documentos a Markdown</h1>
<p class="text-muted mb-0">Sube un archivo, revisa el resultado y descarga tu <code>.md</code>.</p>
</header>

<div class="card p-3 mb-3">
<input type="file" id="fileInput" class="form-control mb-2"
accept=".pdf,.docx,.pptx,.xlsx,.html,.csv" />
<button id="btnConvert" class="btn btn-primary">Convertir</button>
<span id="statusText" class="ms-2 text-muted"></span>
<div class="row g-4">
<section class="col-lg-8" aria-label="Conversor">
<div class="card app-card p-4 mb-4">
<label for="fileInput" class="form-label fw-semibold">Documento</label>
<input type="file" id="fileInput" class="form-control"
accept=".pdf,.docx,.pptx,.xlsx,.html,.csv"
aria-describedby="supportedFormats statusText" />
<div id="supportedFormats" class="form-text">PDF, Word, PowerPoint, Excel, HTML o CSV · máximo 100 MB</div>
<div class="d-flex align-items-center gap-3 mt-3">
<button id="btnConvert" class="btn btn-primary px-4">Convertir a Markdown</button>
<span id="statusText" class="status-text text-muted" role="status" aria-live="polite"></span>
</div>
</div>

<div class="card p-3">
<div class="d-flex justify-content-between mb-2">
<h5>Markdown generado</h5>
<div class="card app-card p-4">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
<div>
<button id="btnCopy" class="btn btn-sm btn-outline-secondary">Copiar</button>
<button id="btnDownload" class="btn btn-sm btn-outline-secondary">Descargar</button>
<button id="btnClear" class="btn btn-sm btn-outline-danger">Limpiar</button>
<p class="eyebrow mb-1">RESULTADO</p>
<h2 id="resultHeading" class="h5 mb-0">Markdown generado</h2>
</div>
<div class="d-flex gap-2">
<button id="btnCopy" class="btn btn-sm btn-outline-secondary" disabled>Copiar</button>
<button id="btnDownload" class="btn btn-sm btn-outline-primary" disabled>Descargar .md</button>
<button id="btnClear" class="btn btn-sm btn-outline-secondary">Limpiar</button>
</div>
</div>
<pre id="markdownOutput" class="border rounded p-3" style="min-height:300px; white-space:pre-wrap;"></pre>
<pre id="markdownOutput" class="markdown-output" aria-label="Contenido Markdown" tabindex="0"></pre>
</div>
</div>
</section>

<div class="col-md-3">
<h5>Historial</h5>
<input type="text" id="searchHistory" class="form-control mb-2" placeholder="Buscar..." />
<ul id="historyList" class="list-group"></ul>
</div>
<aside class="col-lg-4" aria-label="Historial de conversiones">
<div class="card app-card p-4 h-100">
<div class="mb-3">
<p class="eyebrow mb-1">EN ESTE DISPOSITIVO</p>
<h2 class="h5 mb-0">Historial</h2>
</div>
<label class="visually-hidden" for="searchHistory">Buscar en el historial</label>
<input type="search" id="searchHistory" class="form-control mb-3" placeholder="Buscar archivo..." />
<ul id="historyList" class="list-group list-group-flush history-list"></ul>
<p id="historyEmpty" class="text-muted small mt-3 mb-0" hidden>Aún no hay conversiones guardadas.</p>
</div>
</aside>
</div>
</div>

@section Scripts {
<script src="~/js/conversion.js"></script>
<script src="~/js/conversion.js" asp-append-version="true"></script>
}
2 changes: 1 addition & 1 deletion src/Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"DefaultConnection": "Data Source=MarkItDownWeb.db"
},
"MarkItDown": {
"PythonPath": "C:\\Python314\\python.exe"
"PythonPath": "python"
},
"Storage": {
"UploadsFolder": "Uploads",
Expand Down
Loading
Loading