1 /** 2 * MIME store implemented around reading of various files in mime/ subfolder. 3 * 4 * Authors: 5 * $(LINK2 https://github.com/FreeSlave, Roman Chistokhodov) 6 * License: 7 * $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 8 * Copyright: 9 * Roman Chistokhodov, 2015-2016 10 */ 11 12 module mime.stores.subtypexml; 13 14 import mime.common; 15 import mime.store; 16 17 private { 18 import std.algorithm.iteration : filter, map, joiner; 19 import std.array; 20 import std.exception : assumeUnique, collectException; 21 import std.file : isFile; 22 import std.path : buildPath; 23 import std.range : retro; 24 import std.range.interfaces : inputRangeObject; 25 import std.stdio : File; 26 import std.typecons; 27 28 import mime.files.types; 29 import mime.xml : readMediaSubtypeFile; 30 } 31 32 public import mime.files.common; 33 34 /** 35 * Implementation of $(D mime.store.IMimeStore) interface that uses MEDIA/SUBTYPE.xml files from mime/ subfolder to read MIME types. 36 * It does not read any MIME type definitions at the construction time. 37 * Instead MediaSubtypeXmlStore performs parsing of separate files on demand when calling $(D mimeType) or $(D byMimeType). 38 * All parsed definitions are getting cached to avoid re-parsing on every demand. 39 * See_Also: $(D mime.xml.readMediaSubtypeFile) 40 */ 41 final class MediaSubtypeXmlStore : IMimeStore 42 { 43 /** 44 * Params: 45 * mimePaths = Range of paths to base mime/ directories in order from more preferable to less preferable. 46 */ 47 @safe this(const(string)[] mimePaths) nothrow pure 48 { 49 _mimePaths = mimePaths.dup; 50 } 51 52 /** 53 * Find and parse MEDIA/SUBTYPE.xml file(s) for given MIME type name. 54 * If it finds more then one file for the MIME type, merging operation is performed. 55 * Returns: $(D mime.type.MimeType) object parsed from found xml file(s) or null if no file was found or name is invalid. 56 * Throws: $(D mime.xml.XMLMimeException) on format error or $(B std.file.FileException) on file reading error. 57 * See_Also: $(D mime.type.mergeMimeTypes) 58 */ 59 Rebindable!(const(MimeType)) mimeType(const char[] name) 60 { 61 return rebindable(mimeTypeImpl(name)); 62 } 63 64 private const(MimeType) mimeTypeImpl(const char[] name) 65 { 66 if (!isValidMimeTypeName(name)) 67 return null; 68 MimeType* pmimeType = name in _mimeTypes; 69 if (pmimeType) 70 return *pmimeType; 71 foreach(mimePath; _mimePaths.retro) 72 { 73 auto subtypePath = buildPath(mimePath, assumeUnique(name ~ ".xml")); 74 bool isFile; 75 collectException(subtypePath.isFile, isFile); 76 if (isFile) 77 { 78 auto mimeType = readMediaSubtypeFile(subtypePath); 79 pmimeType = name in _mimeTypes; 80 if (pmimeType) 81 { 82 mergeMimeTypesInPlace(*pmimeType, mimeType); 83 } 84 else 85 { 86 addIconNames(mimeType); 87 _mimeTypes[mimeType.name] = mimeType; 88 } 89 } 90 } 91 pmimeType = name in _mimeTypes; 92 if (pmimeType) 93 return *pmimeType; 94 return null; 95 } 96 97 /** 98 * Lazily read MIME types objects. The list of MIME types is read from mime/types file, so it must be present. 99 * Returns: Range of $(D mime.type.MimeType) objects. 100 * Throws: 101 * $(D mime.files.common.MimeFileException) on mime/types file parsing error. 102 * $(D mime.xml.XMLMimeException) on xml format error. 103 * $(B std.file.FileException) on file reading error. 104 * Note: The resulted range may contain duplicates, if some MIME type has multiple definitions across base mime paths. 105 * The duplicates in this case refer to the same object, i.e. $(B is)-equal. 106 */ 107 InputRange!(const(MimeType)) byMimeType() { 108 auto typesPaths = _mimePaths.retro.map!(mimePath => buildPath(mimePath, "types")).filter!(delegate(string typesPath) { 109 bool isFile; 110 collectException(typesPath.isFile, isFile); 111 return isFile; 112 }); 113 114 auto typeNames = typesPaths.map!(typesPath => typesFileReader(File(typesPath, "r").byLineCopy())).joiner; 115 auto mimeTypes = typeNames.map!(type => mimeTypeImpl(type)).filter!(mimeType => mimeType !is null); 116 return inputRangeObject(mimeTypes); 117 } 118 119 private: 120 @safe void addIconNames(MimeType mimeType) 121 { 122 if (!mimeType.icon) 123 mimeType.icon = defaultIconName(mimeType.name); 124 if (!mimeType.genericIcon) 125 mimeType.genericIcon = defaultGenericIconName(mimeType.name); 126 } 127 128 string[] _mimePaths; 129 MimeType[const(char)[]] _mimeTypes; 130 } 131 132 unittest 133 { 134 auto mimePaths = ["./test/mime", "./test/discard", "./test/nonexistent"]; 135 auto store = new MediaSubtypeXmlStore(mimePaths); 136 137 assert(store.mimeType("invalid") is null); 138 assert(store.mimeType("application/nonexistent") is null); 139 140 auto sequenceType = store.mimeType("application/x-hlmdl-sequence"); 141 assert(sequenceType !is null); 142 assert(sequenceType.globs == [MimeGlob("*[0123456789][0123456789].mdl", defaultGlobWeight, false)]); 143 assert(sequenceType.genericIcon == "application-x-hlmdl"); 144 assert(sequenceType is store.mimeType("application/x-hlmdl-sequence")); 145 146 auto quakeSprite = store.mimeType("image/x-qsprite"); 147 assert(quakeSprite !is null); 148 assert(quakeSprite.aliases == ["application/x-qsprite"]); 149 150 import std.algorithm.searching : canFind; 151 assert(store.byMimeType.canFind!((const(MimeType) type, string name) { return type.name == name; })("application/x-pak")); 152 }