Nombre de archivo C # Sanitize

174

Recientemente he estado moviendo un montón de archivos MP3 desde varias ubicaciones a un repositorio. Había estado construyendo los nuevos nombres de archivo usando las etiquetas ID3 (¡gracias, TagLib-Sharp!), Y noté que estaba obteniendo un System.NotSupportedException:

"El formato de la ruta dada no es compatible".

Esto fue generado por cualquiera File.Copy()o Directory.CreateDirectory().

No tardé mucho en darme cuenta de que mis nombres de archivo necesitaban ser desinfectados. Entonces hice lo obvio:

public static string SanitizePath_(string path, char replaceChar)
{
    string dir = Path.GetDirectoryName(path);
    foreach (char c in Path.GetInvalidPathChars())
        dir = dir.Replace(c, replaceChar);

    string name = Path.GetFileName(path);
    foreach (char c in Path.GetInvalidFileNameChars())
        name = name.Replace(c, replaceChar);

    return dir + name;
}

Para mi sorpresa, seguí recibiendo excepciones. Resultó que ':' no está en el conjunto de Path.GetInvalidPathChars(), porque es válido en una raíz de ruta. Supongo que tiene sentido, pero este tiene que ser un problema bastante común. ¿Alguien tiene algún código corto que desinfecte una ruta? Lo más completo que he encontrado con esto, pero parece que probablemente sea exagerado.

    // replaces invalid characters with replaceChar
    public static string SanitizePath(string path, char replaceChar)
    {
        // construct a list of characters that can't show up in filenames.
        // need to do this because ":" is not in InvalidPathChars
        if (_BadChars == null)
        {
            _BadChars = new List<char>(Path.GetInvalidFileNameChars());
            _BadChars.AddRange(Path.GetInvalidPathChars());
            _BadChars = Utility.GetUnique<char>(_BadChars);
        }

        // remove root
        string root = Path.GetPathRoot(path);
        path = path.Remove(0, root.Length);

        // split on the directory separator character. Need to do this
        // because the separator is not valid in a filename.
        List<string> parts = new List<string>(path.Split(new char[]{Path.DirectorySeparatorChar}));

        // check each part to make sure it is valid.
        for (int i = 0; i < parts.Count; i++)
        {
            string part = parts[i];
            foreach (char c in _BadChars)
            {
                part = part.Replace(c, replaceChar);
            }
            parts[i] = part;
        }

        return root + Utility.Join(parts, Path.DirectorySeparatorChar.ToString());
    }

Cualquier mejora para hacer esta función más rápida y menos barroca sería muy apreciada.

Jason Sundram
fuente

Respuestas:

314

Para limpiar un nombre de archivo, puede hacer esto

private static string MakeValidFileName( string name )
{
   string invalidChars = System.Text.RegularExpressions.Regex.Escape( new string( System.IO.Path.GetInvalidFileNameChars() ) );
   string invalidRegStr = string.Format( @"([{0}]*\.+$)|([{0}]+)", invalidChars );

   return System.Text.RegularExpressions.Regex.Replace( name, invalidRegStr, "_" );
}
Andre
fuente
3
La pregunta era sobre rutas, no nombres de archivos, y los caracteres no válidos para estos son diferentes.
Dour High Arch
15
Tal vez, pero este código ciertamente me ayudó cuando tuve el mismo problema :)
mmr
8
Y otro usuario de SO potencialmente excelente va caminando ... Esta función es excelente. Gracias Adrevdm ...
Dan Rosenstark
19
Gran método Sin embargo, no olvide que las palabras reservadas aún lo morderán y se quedará rascándose la cabeza. Fuente: Wikipedia palabras reservadas de nombre de archivo
Spud
8
Los períodos son caracteres no válidos si están al final del nombre del archivo, por lo GetInvalidFileNameCharsque no los incluye. No arroja una excepción en Windows, solo los elimina, pero podría causar un comportamiento inesperado si espera que el período esté allí. Modifiqué la expresión regular para manejar ese caso para .que se considere uno de los caracteres no válidos si está al final de la cadena.
Scott Chamberlain
120

Una solución más corta:

var invalids = System.IO.Path.GetInvalidFileNameChars();
var newName = String.Join("_", origFileName.Split(invalids, StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
DenNukem
fuente
1
@PeterMajeed: TIL que el conteo de líneas comienza en cero :-)
Gary McGill
Esto es mejor que la respuesta principal, especialmente para ASP.NET Core, que podría devolver diferentes caracteres según la plataforma.
Alexei
79

Basado en la excelente respuesta de Andre, pero teniendo en cuenta el comentario de Spud sobre palabras reservadas, hice esta versión:

/// <summary>
/// Strip illegal chars and reserved words from a candidate filename (should not include the directory path)
/// </summary>
/// <remarks>
/// http://stackoverflow.com/questions/309485/c-sharp-sanitize-file-name
/// </remarks>
public static string CoerceValidFileName(string filename)
{
    var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
    var invalidReStr = string.Format(@"[{0}]+", invalidChars);

    var reservedWords = new []
    {
        "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4",
        "COM5", "COM6", "COM7", "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4",
        "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
    };

    var sanitisedNamePart = Regex.Replace(filename, invalidReStr, "_");
    foreach (var reservedWord in reservedWords)
    {
        var reservedWordPattern = string.Format("^{0}\\.", reservedWord);
        sanitisedNamePart = Regex.Replace(sanitisedNamePart, reservedWordPattern, "_reservedWord_.", RegexOptions.IgnoreCase);
    }

    return sanitisedNamePart;
}

Y estas son mis pruebas unitarias

[Test]
public void CoerceValidFileName_SimpleValid()
{
    var filename = @"thisIsValid.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual(filename, result);
}

[Test]
public void CoerceValidFileName_SimpleInvalid()
{
    var filename = @"thisIsNotValid\3\\_3.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("thisIsNotValid_3__3.txt", result);
}

[Test]
public void CoerceValidFileName_InvalidExtension()
{
    var filename = @"thisIsNotValid.t\xt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("thisIsNotValid.t_xt", result);
}

[Test]
public void CoerceValidFileName_KeywordInvalid()
{
    var filename = "aUx.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("_reservedWord_.txt", result);
}

[Test]
public void CoerceValidFileName_KeywordValid()
{
    var filename = "auxillary.txt";
    var result = PathHelper.CoerceValidFileName(filename);
    Assert.AreEqual("auxillary.txt", result);
}
fíat
fuente
1
Esta es una respuesta extremadamente completa, al menos para la parte del nombre de archivo de la pregunta, y merece más votos a favor.
Brian MacKay
2
Sugerencia menor, ya que parece que el método iba en esta dirección: agregue una palabra clave this y se convierte en un método de extensión útil. public static String CoerceValidFileName (este nombre de archivo de cadena)
Ryan McArthur
2
Pequeño error: este método no cambia las palabras reservadas sin extensiones de archivo (p. Ej. COM1), Que también están prohibidas. La solución sugerida sería cambiar el ReservadoWordPattern a "^{0}(\\.|$)"y la cadena de reemplazo a"_reservedWord_$1"
Dehalion
31
string clean = String.Concat(dirty.Split(Path.GetInvalidFileNameChars()));
datos
fuente
55
considerar en String.Concat(dirty...)lugar deJoin(String.Empty...
drzaus
DenNukem ya sugirió esta respuesta: stackoverflow.com/a/13617375/244916 (sin embargo, tenga en cuenta el comentario).
Amigo Pascalou
4

Estoy usando el System.IO.Path.GetInvalidFileNameChars() método para verificar caracteres no válidos y no tengo problemas.

Estoy usando el siguiente código:

foreach( char invalidchar in System.IO.Path.GetInvalidFileNameChars())
{
    filename = filename.Replace(invalidchar, '_');
}
André Leal
fuente
3

Quería retener a los personajes de alguna manera, no solo reemplazar el personaje con un guión bajo.

Una forma en que pensé fue reemplazar los caracteres con caracteres similares que, en mi situación, es poco probable que se usen como caracteres normales. Así que tomé la lista de caracteres no válidos y encontré look-a-likes.

Las siguientes son funciones para codificar y decodificar con look-a-likes.

Este código no incluye una lista completa de todos los caracteres System.IO.Path.GetInvalidFileNameChars (). Por lo tanto, depende de usted extender o utilizar el reemplazo de subrayado para los caracteres restantes.

private static Dictionary<string, string> EncodeMapping()
{
    //-- Following characters are invalid for windows file and folder names.
    //-- \/:*?"<>|
    Dictionary<string, string> dic = new Dictionary<string, string>();
    dic.Add(@"\", "Ì"); // U+OOCC
    dic.Add("/", "Í"); // U+OOCD
    dic.Add(":", "¦"); // U+00A6
    dic.Add("*", "¤"); // U+00A4
    dic.Add("?", "¿"); // U+00BF
    dic.Add(@"""", "ˮ"); // U+02EE
    dic.Add("<", "«"); // U+00AB
    dic.Add(">", "»"); // U+00BB
    dic.Add("|", "│"); // U+2502
    return dic;
}

public static string Escape(string name)
{
    foreach (KeyValuePair<string, string> replace in EncodeMapping())
    {
        name = name.Replace(replace.Key, replace.Value);
    }

    //-- handle dot at the end
    if (name.EndsWith(".")) name = name.CropRight(1) + "°";

    return name;
}

public static string UnEscape(string name)
{
    foreach (KeyValuePair<string, string> replace in EncodeMapping())
    {
        name = name.Replace(replace.Value, replace.Key);
    }

    //-- handle dot at the end
    if (name.EndsWith("°")) name = name.CropRight(1) + ".";

    return name;
}

Puede seleccionar sus propios look-a-likes. Usé la aplicación Mapa de caracteres en Windows para seleccionar la mía%windir%\system32\charmap.exe

A medida que realice ajustes a través del descubrimiento, actualizaré este código.

Valamas
fuente
Tenga en cuenta que hay muchos caracteres que se parecen más a esos, como la forma de ancho completo !"#$%&'()*+,-./:;<=>?@{|}~ u otras formas de ellos como /SOLIDUS y `⁄` FRACTION SLASH que se pueden usar directamente en nombres de archivos sin problemas
phuclv
2

Creo que el problema es que primero invocas Path.GetDirectoryNamela cadena incorrecta. Si esto tiene caracteres que no son de nombre de archivo, .Net no puede decir qué partes de la cadena son directorios y tiros. Tienes que hacer comparaciones de cadenas.

Asumiendo que solo el nombre del archivo es malo, no la ruta completa, intente esto:

public static string SanitizePath(string path, char replaceChar)
{
    int filenamePos = path.LastIndexOf(Path.DirectorySeparatorChar) + 1;
    var sb = new System.Text.StringBuilder();
    sb.Append(path.Substring(0, filenamePos));
    for (int i = filenamePos; i < path.Length; i++)
    {
        char filenameChar = path[i];
        foreach (char c in Path.GetInvalidFileNameChars())
            if (filenameChar.Equals(c))
            {
                filenameChar = replaceChar;
                break;
            }

        sb.Append(filenameChar);
    }

    return sb.ToString();
}
Dour High Arch
fuente
2

He tenido éxito con esto en el pasado.

Agradable, corto y estático :-)

    public static string returnSafeString(string s)
    {
        foreach (char character in Path.GetInvalidFileNameChars())
        {
            s = s.Replace(character.ToString(),string.Empty);
        }

        foreach (char character in Path.GetInvalidPathChars())
        {
            s = s.Replace(character.ToString(), string.Empty);
        }

        return (s);
    }
Helix 88
fuente
2

Hay muchas soluciones de trabajo aquí. solo por completar, aquí hay un enfoque que no usa expresiones regulares, pero usa LINQ:

var invalids = Path.GetInvalidFileNameChars();
filename = invalids.Aggregate(filename, (current, c) => current.Replace(c, '_'));

Además, es una solución muy corta;)

kappadoky
fuente
1
Me encanta una camisa :)
Larry
1

Aquí hay un método eficiente de extensión de carga diferida basado en el código de Andre:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LT
{
    public static class Utility
    {
        static string invalidRegStr;

        public static string MakeValidFileName(this string name)
        {
            if (invalidRegStr == null)
            {
                var invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()));
                invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);
            }

            return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, "_");
        }
    }
}
Bryan Legend
fuente
0

Su código sería más limpio si agregara el directorio y el nombre de archivo y los desinfecte en lugar de desinfectarlos de forma independiente. En cuanto a desinfectar:: solo toma el segundo carácter de la cadena. Si es igual a "replacechar", reemplácelo con dos puntos. Dado que esta aplicación es para su propio uso, dicha solución debería ser perfectamente suficiente.

Brian
fuente
-1
using System;
using System.IO;
using System.Linq;
using System.Text;

public class Program
{
    public static void Main()
    {
        try
        {
            var badString = "ABC\\DEF/GHI<JKL>MNO:PQR\"STU\tVWX|YZA*BCD?EFG";
            Console.WriteLine(badString);
            Console.WriteLine(SanitizeFileName(badString, '.'));
            Console.WriteLine(SanitizeFileName(badString));
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    private static string SanitizeFileName(string fileName, char? replacement = null)
    {
        if (fileName == null) { return null; }
        if (fileName.Length == 0) { return ""; }

        var sb = new StringBuilder();
        var badChars = Path.GetInvalidFileNameChars().ToList();

        foreach (var @char in fileName)
        {
            if (badChars.Contains(@char)) 
            {
                if (replacement.HasValue)
                {
                    sb.Append(replacement.Value);
                }
                continue; 
            }
            sb.Append(@char);
        }
        return sb.ToString();
    }
}
Ralf
fuente