├── .editorconfig ├── .gitignore ├── Dockerfile ├── LICENSE ├── MinimalNugetServer.sln ├── MinimalNugetServer ├── .editorconfig ├── ContentFacades │ ├── CachedContentFacade.cs │ ├── IContentFacade.cs │ ├── LoadAllContentFacade.cs │ └── LoadNothingContentFacade.cs ├── Extensions.cs ├── Globals.cs ├── MasterData.cs ├── MinimalNugetServer.csproj ├── Program.cs ├── RequestProcessorBase.cs ├── Structures.cs ├── Utils.cs ├── Version2RequestProcessor.cs ├── Version3RequestProcessor.cs ├── configuration.json └── scripts │ ├── make_publishable.bat │ ├── make_publishable.sh │ ├── stahp.sh │ └── start.sh └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.cs, *.json] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | bin/Debug/ 3 | bin/Release/ 4 | obj/Debug/ 5 | obj/Release/ 6 | project.lock.json 7 | 8 | .vs/ 9 | *.xproj.user 10 | samples/NugetPackageConsumingApp1/bin/Debug/ 11 | samples/NugetPackageConsumingApp1/bin/Release/ 12 | samples/NugetPackageConsumingApp1/obj/Debug/ 13 | samples/NugetPackageConsumingApp1/obj/Release/ 14 | MinimalNugetServer/bin/ 15 | MinimalNugetServer/obj/ 16 | samples/NugetPackageConsumingApp1/packages/ 17 | MinimalNugetServer/GitCommitInfo.cs 18 | 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore-build:2.0.0-stretch AS publish 2 | 3 | ENV ASPNETCORE_URLS http://*:80 4 | ENV ASPNETCORE_ENVIRONMENT "Production" 5 | 6 | COPY . /src 7 | WORKDIR /src/MinimalNugetServer 8 | 9 | RUN dotnet restore 10 | RUN dotnet publish --output /src/out 11 | 12 | FROM microsoft/aspnetcore:2.0.0-stretch 13 | 14 | ENV ASPNETCORE_URLS http://*:80 15 | ENV ASPNETCORE_ENVIRONMENT "Production" 16 | 17 | WORKDIR /dotnetapp 18 | COPY --from=publish /src/out . 19 | 20 | EXPOSE 80/tcp 21 | 22 | ENTRYPOINT ["dotnet", "MinimalNugetServer.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sebastien ROBERT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MinimalNugetServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.13 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalNugetServer", "MinimalNugetServer\MinimalNugetServer.csproj", "{2B38A02F-5E8B-401D-A966-597066B412F1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|x64.ActiveCfg = Debug|Any CPU 21 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|x64.Build.0 = Debug|Any CPU 22 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|x86.ActiveCfg = Debug|Any CPU 23 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Debug|x86.Build.0 = Debug|Any CPU 24 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|x64.ActiveCfg = Release|Any CPU 27 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|x64.Build.0 = Release|Any CPU 28 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|x86.ActiveCfg = Release|Any CPU 29 | {2B38A02F-5E8B-401D-A966-597066B412F1}.Release|x86.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /MinimalNugetServer/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | -------------------------------------------------------------------------------- /MinimalNugetServer/ContentFacades/CachedContentFacade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace MinimalNugetServer.ContentFacades 10 | { 11 | public class CachedContentFacade : IContentFacadeAccessor 12 | { 13 | private readonly Dictionary contentIds = new Dictionary(); 14 | private readonly MemoryCacheEntryOptions cacheEntryOptions; 15 | 16 | private IMemoryCache cache = new MemoryCache(DefaultMemoryCacheOptions.Default); 17 | 18 | /// 19 | /// Initializes the instance. 20 | /// 21 | /// The sliding expiration delay of individual entries, in seconds. 22 | public CachedContentFacade(uint cacheSlidingDuration) 23 | { 24 | cacheEntryOptions = new MemoryCacheEntryOptions 25 | { 26 | SlidingExpiration = TimeSpan.FromSeconds((double)cacheSlidingDuration) 27 | }; 28 | } 29 | 30 | public void Add(string contentId, string fullFilePath) 31 | { 32 | contentIds.Add(contentId, fullFilePath); 33 | } 34 | 35 | public void Clear() 36 | { 37 | contentIds.Clear(); 38 | 39 | // this seems to be the simplest way to clear the cache :/ 40 | cache.Dispose(); 41 | cache = new MemoryCache(DefaultMemoryCacheOptions.Default); 42 | } 43 | 44 | public bool TryGetValue(string contentId, out byte[] content) 45 | { 46 | if (cache.TryGetValue(contentId, out content) == false) 47 | { 48 | try 49 | { 50 | content = File.ReadAllBytes(contentIds[contentId]); 51 | cache.Set(contentId, content, cacheEntryOptions); 52 | } 53 | catch 54 | { 55 | return false; 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | 62 | private class DefaultMemoryCacheOptions : IOptions 63 | { 64 | public static readonly DefaultMemoryCacheOptions Default = new DefaultMemoryCacheOptions() 65 | { 66 | Value = new MemoryCacheOptions() 67 | }; 68 | 69 | public MemoryCacheOptions Value { get; private set; } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MinimalNugetServer/ContentFacades/IContentFacade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace MinimalNugetServer.ContentFacades 7 | { 8 | public interface IContentFacade 9 | { 10 | bool TryGetValue(string contentId, out byte[] content); 11 | } 12 | 13 | public interface IContentFacadeAccessor : IContentFacade 14 | { 15 | void Add(string contentId, string fullFilePath); 16 | void Clear(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MinimalNugetServer/ContentFacades/LoadAllContentFacade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace MinimalNugetServer.ContentFacades 8 | { 9 | public class LoadAllContentFacade : IContentFacadeAccessor 10 | { 11 | private readonly Dictionary contents = new Dictionary(); 12 | 13 | public void Add(string contentId, string fullFilePath) 14 | { 15 | contents.Add(contentId, File.ReadAllBytes(fullFilePath)); 16 | } 17 | 18 | public void Clear() 19 | { 20 | contents.Clear(); 21 | } 22 | 23 | public bool TryGetValue(string contentId, out byte[] content) 24 | { 25 | return contents.TryGetValue(contentId, out content); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MinimalNugetServer/ContentFacades/LoadNothingContentFacade.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace MinimalNugetServer.ContentFacades 8 | { 9 | public class LoadNothingContentFacade : IContentFacadeAccessor 10 | { 11 | private readonly Dictionary contentIds = new Dictionary(); 12 | 13 | public void Add(string contentId, string fullFilePath) 14 | { 15 | contentIds.Add(contentId, fullFilePath); 16 | } 17 | 18 | public void Clear() 19 | { 20 | contentIds.Clear(); 21 | } 22 | 23 | public bool TryGetValue(string contentId, out byte[] content) 24 | { 25 | string fullFilePath; 26 | 27 | if (contentIds.TryGetValue(contentId, out fullFilePath) == false) 28 | { 29 | content = null; 30 | return false; 31 | } 32 | 33 | try 34 | { 35 | content = File.ReadAllBytes(fullFilePath); 36 | return true; 37 | } 38 | catch 39 | { 40 | content = null; 41 | return false; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MinimalNugetServer/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace MinimalNugetServer 5 | { 6 | public static class VersionExtensions 7 | { 8 | public static Version Normalize(this Version version) 9 | { 10 | return new Version( 11 | Math.Max(0, version.Major), 12 | Math.Max(0, version.Minor), 13 | Math.Max(0, version.Build), 14 | Math.Max(0, version.Revision) 15 | ); 16 | } 17 | } 18 | 19 | public static class QueryCollectionExtensions 20 | { 21 | public static string GetFirst(this IQueryCollection query, string key, string defaultValue) 22 | { 23 | var values = query[key]; 24 | 25 | if (values.Count == 0) 26 | return defaultValue; 27 | 28 | return values[0]; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MinimalNugetServer/Globals.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml.Linq; 3 | 4 | namespace MinimalNugetServer 5 | { 6 | public static class Globals 7 | { 8 | public static readonly object ConsoleLock = new object(); 9 | public static readonly VersionInfo[] EmptyVersionInfoArray = new VersionInfo[0]; 10 | } 11 | 12 | public static class Characters 13 | { 14 | public static readonly char[] UrlPathSeparator = new char[] { '/' }; 15 | public static readonly char[] SingleQuote = new char[] { '\'' }; 16 | public static readonly char[] Coma = new char[] { ',' }; 17 | public static readonly char[] Dot = new char[] { '.' }; 18 | } 19 | 20 | public static class XmlNamespaces 21 | { 22 | public static readonly XNamespace xmlns = "http://www.w3.org/2005/Atom"; 23 | public static readonly XNamespace baze = "https://www.nuget.org/api/v2/curated-feeds/microsoftdotnet"; 24 | public static readonly XNamespace m = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; 25 | public static readonly XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices"; 26 | public static readonly XNamespace georss = "http://www.georss.org/georss"; 27 | public static readonly XNamespace gml = "http://www.opengis.net/gml"; 28 | } 29 | 30 | public static class XmlElements 31 | { 32 | public static readonly XName feed = XmlNamespaces.xmlns + "feed"; 33 | public static readonly XName entry = XmlNamespaces.xmlns + "entry"; 34 | public static readonly XName id = XmlNamespaces.xmlns + "id"; 35 | public static readonly XName content = XmlNamespaces.xmlns + "content"; 36 | 37 | public static readonly XName m_count = XmlNamespaces.m + "count"; 38 | public static readonly XName m_properties = XmlNamespaces.m + "properties"; 39 | 40 | public static readonly XName d_id = XmlNamespaces.d + "Id"; 41 | public static readonly XName d_version = XmlNamespaces.d + "Version"; 42 | 43 | public static readonly XName baze = XNamespace.Xmlns + "base"; 44 | public static readonly XName m = XNamespace.Xmlns + "m"; 45 | public static readonly XName d = XNamespace.Xmlns + "d"; 46 | public static readonly XName georss = XNamespace.Xmlns + "georss"; 47 | public static readonly XName gml = XNamespace.Xmlns + "gml"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /MinimalNugetServer/MasterData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Threading; 8 | using Microsoft.Extensions.Configuration; 9 | using MinimalNugetServer.ContentFacades; 10 | 11 | namespace MinimalNugetServer 12 | { 13 | public enum CacheType 14 | { 15 | NoCacheLoadAll, 16 | Cache, 17 | NoCacheLoadNothing, 18 | } 19 | 20 | public struct CacheStrategy 21 | { 22 | public CacheType CacheType; 23 | public uint CacheEntryExpiration; // in seconds 24 | } 25 | 26 | public class MasterData 27 | { 28 | private IReadOnlyList packages; 29 | private readonly IContentFacadeAccessor contentFacade; 30 | 31 | private readonly string packagesPath; 32 | private readonly bool makeReadonly; 33 | 34 | private ReaderWriterLockSlim packagesProcessingLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); 35 | private const int PackageProcessingThrottleDelay = 1000; 36 | private Timer packageProcessingThrottleTimer; 37 | 38 | public MasterData(IConfiguration nugetConfig, CacheStrategy cacheStrategy) 39 | { 40 | if (nugetConfig == null) 41 | throw new ArgumentException($"Invalid '{nameof(nugetConfig)}' argument.", nameof(nugetConfig)); 42 | 43 | packagesPath = nugetConfig["packages"]; 44 | makeReadonly = bool.Parse(nugetConfig["makeReadonly"]); 45 | 46 | if (cacheStrategy.CacheType == CacheType.NoCacheLoadAll) 47 | contentFacade = new LoadAllContentFacade(); 48 | else if (cacheStrategy.CacheType == CacheType.NoCacheLoadNothing) 49 | contentFacade = new LoadNothingContentFacade(); 50 | else if (cacheStrategy.CacheType == CacheType.Cache) 51 | contentFacade = new CachedContentFacade(cacheStrategy.CacheEntryExpiration); 52 | else 53 | throw new NotSupportedException($"Cache type '{cacheStrategy.CacheType}' not supported yet."); 54 | 55 | ProcessPackageFiles(); 56 | WatchPackagesFolder(); 57 | } 58 | 59 | public TResult Use(T1 arg1, Func, IContentFacade, T1, TResult> action) 60 | { 61 | if (action == null) 62 | throw new ArgumentNullException(nameof(action)); 63 | 64 | packagesProcessingLock.EnterReadLock(); 65 | 66 | try 67 | { 68 | return action(packages, contentFacade, arg1); 69 | } 70 | finally 71 | { 72 | packagesProcessingLock.ExitReadLock(); 73 | } 74 | } 75 | 76 | public TResult Use(T1 arg1, T2 arg2, Func,IContentFacade, T1, T2, TResult> action) 77 | { 78 | if (action == null) 79 | throw new ArgumentNullException(nameof(action)); 80 | 81 | packagesProcessingLock.EnterReadLock(); 82 | 83 | try 84 | { 85 | return action(packages, contentFacade, arg1, arg2); 86 | } 87 | finally 88 | { 89 | packagesProcessingLock.ExitReadLock(); 90 | } 91 | } 92 | 93 | private readonly FileSystemWatcher watcher = new FileSystemWatcher(); 94 | 95 | private void WatchPackagesFolder() 96 | { 97 | watcher.Path = packagesPath; 98 | watcher.NotifyFilter = 99 | NotifyFilters.Attributes | 100 | NotifyFilters.FileName | 101 | NotifyFilters.DirectoryName | 102 | NotifyFilters.Attributes | 103 | NotifyFilters.Size | 104 | NotifyFilters.LastWrite | 105 | NotifyFilters.CreationTime; 106 | watcher.Filter = "*.nupkg"; 107 | watcher.Changed += OnPackagesFolderChanged; 108 | watcher.Created += OnPackagesFolderChanged; 109 | watcher.Deleted += OnPackagesFolderChanged; 110 | watcher.Renamed += OnPackagesFolderChanged; 111 | watcher.Error += OnError; 112 | watcher.IncludeSubdirectories = true; 113 | watcher.EnableRaisingEvents = true; 114 | 115 | packageProcessingThrottleTimer = new Timer(OnPackageProcessingThrottleTimeout, null, Timeout.Infinite, Timeout.Infinite); 116 | } 117 | 118 | private void OnPackageProcessingThrottleTimeout(object ignore) 119 | { 120 | packagesProcessingLock.EnterWriteLock(); 121 | watcher.EnableRaisingEvents = false; 122 | 123 | try 124 | { 125 | ProcessPackageFiles(); 126 | } 127 | finally 128 | { 129 | watcher.EnableRaisingEvents = true; 130 | packagesProcessingLock.ExitWriteLock(); 131 | } 132 | } 133 | 134 | private void OnError(object sender, ErrorEventArgs e) 135 | { 136 | lock (Globals.ConsoleLock) 137 | Console.WriteLine($"OnError [{e.GetException().Message}]"); 138 | } 139 | 140 | private void OnPackagesFolderChanged(object sender, FileSystemEventArgs e) 141 | { 142 | lock (Globals.ConsoleLock) 143 | Console.WriteLine($"Restarting package processing throttle timer"); 144 | 145 | packageProcessingThrottleTimer.Change(PackageProcessingThrottleDelay, Timeout.Infinite); 146 | } 147 | 148 | private void ProcessPackageFiles() 149 | { 150 | lock (Globals.ConsoleLock) 151 | Console.WriteLine("Processing packages..."); 152 | 153 | string[] allFiles = Directory.GetFiles(packagesPath, "*.nupkg", SearchOption.AllDirectories); 154 | 155 | if (makeReadonly && RuntimeInformation.IsOSPlatform(OSPlatform.Windows) == false) 156 | { 157 | foreach (string filename in allFiles) 158 | Utils.ChMod("555", filename); 159 | } 160 | 161 | var groups = from fullFilePath in allFiles 162 | where fullFilePath.EndsWith(".symbols.nupkg") == false 163 | where File.Exists(fullFilePath) 164 | let idVersion = Utils.SplitIdAndVersion(Path.GetFileNameWithoutExtension(fullFilePath)) 165 | select new 166 | { 167 | Id = idVersion.Id, 168 | Version = idVersion.Version, 169 | FullFilePath = fullFilePath, 170 | ContentId = ((uint)fullFilePath.GetHashCode()).ToString(), 171 | Content = File.ReadAllBytes(fullFilePath) 172 | } 173 | into x 174 | group x by x.Id; 175 | 176 | contentFacade.Clear(); 177 | foreach (var group in groups) 178 | { 179 | foreach (var info in group) 180 | contentFacade.Add(info.ContentId, info.FullFilePath); 181 | } 182 | 183 | PackageInfo[] localPackages = (from g in groups 184 | let versions = g.Select(x => new VersionInfo { Version = x.Version, ContentId = x.ContentId }).OrderBy(x => x.Version).ToArray() 185 | select new PackageInfo 186 | { 187 | Id = g.Key, 188 | Versions = versions, 189 | LatestVersion = versions[versions.Length - 1].Version, 190 | LatestContentId = versions[versions.Length - 1].ContentId 191 | }) 192 | .OrderBy(x => x.Id) 193 | .ToArray(); 194 | 195 | packages = new ReadOnlyCollection(localPackages); 196 | 197 | lock (Globals.ConsoleLock) 198 | { 199 | Console.WriteLine("Processing packages done!"); 200 | Console.WriteLine(); 201 | } 202 | } 203 | 204 | public int FindPackageIndex(string packageId) 205 | { 206 | if (string.IsNullOrWhiteSpace(packageId)) 207 | return -1; 208 | 209 | packagesProcessingLock.EnterReadLock(); 210 | 211 | try 212 | { 213 | return FindPackageIndexNotSafe(packageId); 214 | } 215 | finally 216 | { 217 | packagesProcessingLock.ExitReadLock(); 218 | } 219 | } 220 | 221 | private int FindPackageIndexNotSafe(string packageId) 222 | { 223 | for (int i = 0; i < packages.Count; i++) 224 | { 225 | if (string.Equals(packages[i].Id, packageId, StringComparison.OrdinalIgnoreCase)) 226 | return i; 227 | } 228 | 229 | return -1; 230 | } 231 | 232 | public string FindContentId(string packageId, Version version) 233 | { 234 | if (string.IsNullOrWhiteSpace(packageId)) 235 | return null; 236 | 237 | packagesProcessingLock.EnterReadLock(); 238 | 239 | try 240 | { 241 | return FindContentIdNotSafe(packageId, version); 242 | } 243 | finally 244 | { 245 | packagesProcessingLock.ExitReadLock(); 246 | } 247 | } 248 | 249 | private string FindContentIdNotSafe(string packageId, Version version) 250 | { 251 | for (int i = 0; i < packages.Count; i++) 252 | { 253 | if (string.Equals(packages[i].Id, packageId, StringComparison.OrdinalIgnoreCase)) 254 | { 255 | VersionInfo[] versions = packages[i].Versions; 256 | for (int j = 0; j < versions.Length; j++) 257 | { 258 | if (versions[j].Version == version) 259 | return versions[j].ContentId; 260 | } 261 | } 262 | } 263 | 264 | return null; 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /MinimalNugetServer/MinimalNugetServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | Sebastien ROBERT 7 | 8 | A minimal but cross-platform implementation of a NuGet server, running on .NET Core 9 | nuget server nuget-server cross-platform 10 | 1.1.0 11 | Sebastien ROBERT 12 | https://raw.githubusercontent.com/TanukiSharp/MinimalNugetServer/master/LICENSE 13 | https://github.com/TanukiSharp/MinimalNugetServer 14 | 15 | 16 | 17 | True 18 | 19 | 20 | 21 | 22 | True 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /MinimalNugetServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.PlatformAbstractions; 9 | 10 | namespace MinimalNugetServer 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | string configurationFile = "configuration.json"; 17 | 18 | if (args.Length == 1) 19 | configurationFile = args[0]; 20 | 21 | IConfigurationBuilder builder = new ConfigurationBuilder() 22 | .SetBasePath(Directory.GetCurrentDirectory()) 23 | .AddJsonFile(configurationFile, optional: false); 24 | 25 | IConfigurationRoot configuration = builder.Build(); 26 | 27 | new Program().Run(configuration); 28 | } 29 | 30 | private static CacheStrategy LoadCacheStrategy(IConfigurationRoot config) 31 | { 32 | CacheType cacheType = CacheType.NoCacheLoadAll; 33 | uint cacheDuration = 24 * 3600; // 24 hours 34 | 35 | string strCacheType = config["cache:type"]; 36 | 37 | if (Enum.TryParse(strCacheType, true, out CacheType tempCacheType)) 38 | cacheType = tempCacheType; 39 | 40 | string strCacheDuration = config["cache:duration"]; 41 | 42 | if (uint.TryParse(strCacheDuration, out uint tempCacheDuration)) 43 | cacheDuration = tempCacheDuration; 44 | 45 | lock (Globals.ConsoleLock) 46 | { 47 | Console.Write($"Cache strategy: {cacheType}"); 48 | if (cacheType == CacheType.Cache) 49 | Console.Write($" ({cacheDuration} seconds)"); 50 | Console.WriteLine(); 51 | } 52 | 53 | return new CacheStrategy 54 | { 55 | CacheType = cacheType, 56 | CacheEntryExpiration = cacheDuration, 57 | }; 58 | } 59 | 60 | // =================================================================================== 61 | 62 | private readonly RequestProcessorBase[] requestProcessors = new RequestProcessorBase[] 63 | { 64 | new Version2RequestProcessor(), 65 | new Version3RequestProcessor() 66 | }; 67 | 68 | private void Run(IConfigurationRoot config) 69 | { 70 | string appName = PlatformServices.Default.Application.ApplicationName; 71 | string appVersion = PlatformServices.Default.Application.ApplicationVersion; 72 | MasterData masterData; 73 | 74 | lock (Globals.ConsoleLock) 75 | { 76 | Console.WriteLine($"{appName} {appVersion}"); 77 | Console.WriteLine(); 78 | } 79 | 80 | CacheStrategy cacheStrategy = LoadCacheStrategy(config); 81 | 82 | masterData = new MasterData(config.GetSection("nuget"), cacheStrategy); 83 | 84 | foreach (RequestProcessorBase requestProcessor in requestProcessors) 85 | requestProcessor.Initialize(masterData); 86 | 87 | string url = config["server:url"]; 88 | 89 | new WebHostBuilder() 90 | .UseKestrel() 91 | .UseUrls(url) 92 | .Configure(app => app.Run(OnRequest)) 93 | .Build() 94 | .Run(); 95 | } 96 | 97 | private async Task OnRequest(HttpContext context) 98 | { 99 | lock (Globals.ConsoleLock) 100 | { 101 | Console.WriteLine(); 102 | Console.WriteLine("---"); 103 | Console.WriteLine($"{nameof(context.Request.ContentLength)}: {context.Request.ContentLength}"); 104 | Console.WriteLine($"{nameof(context.Request.ContentType)}: {context.Request.ContentType}"); 105 | Console.WriteLine($"{nameof(context.Request.Method)}: {context.Request.Method}"); 106 | Console.WriteLine($"{nameof(context.Request.Path)}: {context.Request.Path}"); 107 | Console.WriteLine($"{nameof(context.Request.PathBase)}: {context.Request.PathBase}"); 108 | Console.WriteLine($"{nameof(context.Request.Protocol)}: {context.Request.Protocol}"); 109 | Console.WriteLine($"{nameof(context.Request.QueryString)}: {context.Request.QueryString}"); 110 | Console.WriteLine("---"); 111 | Console.WriteLine(); 112 | } 113 | 114 | foreach (RequestProcessorBase requestProcessor in requestProcessors) 115 | { 116 | if (context.Request.Path.StartsWithSegments(requestProcessor.Segment)) 117 | { 118 | await requestProcessor.ProcessRequest(context); 119 | return; 120 | } 121 | } 122 | 123 | lock (Globals.ConsoleLock) 124 | { 125 | Console.ForegroundColor = ConsoleColor.Red; 126 | Console.WriteLine("Unprocessed request"); 127 | Console.ResetColor(); 128 | } 129 | 130 | context.Response.StatusCode = 404; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /MinimalNugetServer/RequestProcessorBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace MinimalNugetServer 6 | { 7 | public abstract class RequestProcessorBase 8 | { 9 | public MasterData MasterData { get; private set; } 10 | 11 | public virtual void Initialize(MasterData masterData) 12 | { 13 | if (masterData == null) 14 | throw new ArgumentNullException(nameof(masterData)); 15 | 16 | MasterData = masterData; 17 | } 18 | 19 | public abstract PathString Segment { get; } 20 | public abstract Task ProcessRequest(HttpContext context); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MinimalNugetServer/Structures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MinimalNugetServer 5 | { 6 | public struct VersionInfo 7 | { 8 | public Version Version; 9 | public string ContentId; 10 | } 11 | 12 | public struct PackageInfo 13 | { 14 | public string Id; 15 | public Version LatestVersion; 16 | public string LatestContentId; 17 | public VersionInfo[] Versions; 18 | } 19 | 20 | public struct IdVersion 21 | { 22 | public string Id; 23 | public Version Version; 24 | } 25 | 26 | public struct Version3IndexInfo 27 | { 28 | public string IdSegment; 29 | public string Type; 30 | 31 | public Version3IndexInfo(string idSegment, string type) 32 | { 33 | IdSegment = idSegment; 34 | Type = type; 35 | } 36 | 37 | public IDictionary ToSerializable(string urlPrefix) 38 | { 39 | return new Dictionary 40 | { 41 | ["@id"] = $"{urlPrefix.TrimEnd(Characters.UrlPathSeparator)}/{IdSegment}", 42 | ["@type"] = Type, 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MinimalNugetServer/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace MinimalNugetServer 8 | { 9 | public static class Utils 10 | { 11 | public static void ChMod(string arg, string filename) 12 | { 13 | if (arg == null) 14 | throw new ArgumentNullException(nameof(arg)); 15 | if (filename == null) 16 | throw new ArgumentNullException(nameof(filename)); 17 | 18 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 19 | Process.Start("chmod", $"{arg.Trim()} \"{filename}\""); 20 | } 21 | 22 | public static IdVersion SplitIdAndVersion(string filename) 23 | { 24 | var parts = filename.Split(Characters.Dot); 25 | 26 | var versionParts = new List(); 27 | int num; 28 | 29 | int i; 30 | for (i = parts.Length - 1; i >= 0; i--) 31 | { 32 | if (int.TryParse(parts[i], out num) == false) 33 | break; 34 | 35 | versionParts.Insert(0, num); 36 | } 37 | 38 | if (versionParts.Count < 4) 39 | versionParts.AddRange(Enumerable.Repeat(0, 4 - versionParts.Count)); 40 | 41 | return new IdVersion 42 | { 43 | Id = string.Join(".", parts, 0, i + 1), 44 | Version = new Version(versionParts[0], versionParts[1], versionParts[2], versionParts[3]) 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MinimalNugetServer/Version2RequestProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Xml.Linq; 8 | using Microsoft.AspNetCore.Http; 9 | using MinimalNugetServer.ContentFacades; 10 | 11 | namespace MinimalNugetServer 12 | { 13 | public class Version2RequestProcessor : RequestProcessorBase 14 | { 15 | private int downloadCounter; 16 | private int searchCounter; 17 | private int packagesCounter; 18 | private int findCounter; 19 | 20 | private static readonly PathString DownloadPrefix = new PathString("/download"); 21 | 22 | public override PathString Segment { get; } = new PathString("/v2"); 23 | 24 | public override async Task ProcessRequest(HttpContext context) 25 | { 26 | PathString path; 27 | 28 | if (context.Request.Path.StartsWithSegments(Segment, out path) == false) 29 | return; 30 | 31 | PathString downloadPath; 32 | 33 | if (path.StartsWithSegments(DownloadPrefix, out downloadPath)) 34 | { 35 | Interlocked.Increment(ref downloadCounter); 36 | ReportCounters(); 37 | await ProcessDownload(context, downloadPath.Value); 38 | return; 39 | } 40 | else if (path.Value == "/Search()") 41 | { 42 | Interlocked.Increment(ref searchCounter); 43 | ReportCounters(); 44 | await ProcessSearch(context); 45 | return; 46 | } 47 | else if (path.Value.StartsWith("/Packages(")) 48 | { 49 | Interlocked.Increment(ref packagesCounter); 50 | ReportCounters(); 51 | await ProcessPackages(context); 52 | return; 53 | } 54 | else if (path.Value == "/FindPackagesById()") 55 | { 56 | Interlocked.Increment(ref findCounter); 57 | ReportCounters(); 58 | await FindPackage(context); 59 | return; 60 | } 61 | } 62 | 63 | private void ReportCounters() 64 | { 65 | lock (Globals.ConsoleLock) 66 | Console.WriteLine($"download: {downloadCounter}, search: {searchCounter}, packages: {packagesCounter}, find: {findCounter}"); 67 | } 68 | 69 | private async Task ProcessDownload(HttpContext context, string downloadPath) 70 | { 71 | byte[] content = MasterData.Use(downloadPath, (packageInfo, contentFacade, _downloadPath) => 72 | { 73 | byte[] _content; 74 | if (contentFacade.TryGetValue(_downloadPath.TrimStart(Characters.UrlPathSeparator), out _content)) 75 | return _content; 76 | return null; 77 | }); 78 | 79 | if (content == null) 80 | { 81 | context.Response.StatusCode = 404; 82 | lock (Globals.ConsoleLock) 83 | Console.WriteLine($"Download: return {context.Response.StatusCode}"); 84 | return; 85 | } 86 | 87 | context.Response.StatusCode = 200; 88 | context.Response.ContentType = "application/octet-stream"; 89 | await context.Response.Body.WriteAsync(content, 0, content.Length); 90 | } 91 | 92 | private Task ProcessSearch(HttpContext context) 93 | { 94 | return MasterData.Use(context, ProcessSearchSafe); 95 | } 96 | 97 | private async Task ProcessSearchSafe(IReadOnlyList packages, IContentFacade unused, HttpContext context) 98 | { 99 | // targetFramework = '...' 100 | // $filter = IsLatestVersion 101 | 102 | IQueryCollection query = context.Request.Query; 103 | 104 | int skip = 0; 105 | int top = 0; 106 | 107 | bool check = 108 | int.TryParse(query.GetFirst("$skip", string.Empty), out skip) && 109 | int.TryParse(query.GetFirst("$top", string.Empty), out top); 110 | 111 | if (check == false) 112 | { 113 | context.Response.StatusCode = 400; 114 | lock (Globals.ConsoleLock) 115 | Console.WriteLine($"Search: return {context.Response.StatusCode}"); 116 | return; 117 | } 118 | 119 | string searchTerm = query.GetFirst("searchTerm", "").Trim(Characters.SingleQuote); 120 | 121 | var packagesMatchingByIdIndices = new List(); 122 | for (int packageIndex = 0; packageIndex < packages.Count; packageIndex++) 123 | { 124 | if (packages[packageIndex].Id.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1) 125 | packagesMatchingByIdIndices.Add(packageIndex); 126 | } 127 | 128 | int totalPacketCount = packagesMatchingByIdIndices.Count; 129 | 130 | top = Math.Min(top, totalPacketCount); 131 | 132 | var matchingPackageIndices = packagesMatchingByIdIndices 133 | .Skip(skip) 134 | .Take(top); 135 | 136 | lock (Globals.ConsoleLock) 137 | { 138 | Console.WriteLine(); 139 | Console.WriteLine("--- Search -----------------------------------"); 140 | foreach (var index in matchingPackageIndices) 141 | Console.WriteLine($"{packages[index].Id} - {packages[index].LatestVersion}"); 142 | Console.WriteLine("----------------------------------------------"); 143 | Console.WriteLine(); 144 | } 145 | 146 | var doc = new XElement( 147 | XmlElements.feed, 148 | new XAttribute(XmlElements.baze, XmlNamespaces.baze), 149 | new XAttribute(XmlElements.m, XmlNamespaces.m), 150 | new XAttribute(XmlElements.d, XmlNamespaces.d), 151 | new XAttribute(XmlElements.georss, XmlNamespaces.georss), 152 | new XAttribute(XmlElements.gml, XmlNamespaces.gml), 153 | new XElement(XmlElements.m_count, totalPacketCount.ToString()), 154 | matchingPackageIndices.Select(idx => 155 | new XElement( 156 | XmlElements.entry, 157 | new XElement(XmlElements.id, $"{context.Request.Scheme}://{context.Request.Host}{Segment}/Packages(Id='{packages[idx].Id}',Version='{packages[idx].LatestVersion}')"), 158 | new XElement( 159 | XmlElements.content, 160 | new XAttribute("type", "application/zip"), 161 | new XAttribute("src", $"{context.Request.Scheme}://{context.Request.Host}{Segment}/download/{packages[idx].LatestContentId}") 162 | ), 163 | new XElement( 164 | XmlElements.m_properties, 165 | new XElement(XmlElements.d_id, packages[idx].Id), 166 | new XElement(XmlElements.d_version, packages[idx].LatestVersion) 167 | ) 168 | ) 169 | ) 170 | ); 171 | 172 | var bytes = Encoding.UTF8.GetBytes(doc.ToString(SaveOptions.DisableFormatting)); 173 | 174 | context.Response.StatusCode = 200; 175 | context.Response.ContentType = "application/xml"; 176 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 177 | } 178 | 179 | private async Task ProcessPackages(HttpContext context) 180 | { 181 | var path = Uri.UnescapeDataString(context.Request.Path.Value); 182 | 183 | int start = path.IndexOf('('); 184 | if (start == -1) 185 | { 186 | context.Response.StatusCode = 400; 187 | lock (Globals.ConsoleLock) 188 | Console.WriteLine($"Packages: return {context.Response.StatusCode}"); 189 | return; 190 | } 191 | 192 | start++; 193 | 194 | int end = path.IndexOf(')', start); 195 | if (end == -1) 196 | { 197 | context.Response.StatusCode = 400; 198 | lock (Globals.ConsoleLock) 199 | Console.WriteLine($"Packages: return {context.Response.StatusCode}"); 200 | return; 201 | } 202 | 203 | string id = null; 204 | Version version = null; 205 | 206 | var parts = path.Substring(start, end - start).Split(Characters.Coma); 207 | 208 | foreach (var part in parts) 209 | { 210 | var kv = part.Split(new[] { '=' }, 2); 211 | 212 | if (string.Equals(kv[0], "id", StringComparison.OrdinalIgnoreCase)) 213 | id = kv[1].Trim(Characters.SingleQuote); 214 | else if (string.Equals(kv[0], "version", StringComparison.OrdinalIgnoreCase)) 215 | Version.TryParse(kv[1].Trim(Characters.SingleQuote), out version); 216 | } 217 | 218 | if (string.IsNullOrWhiteSpace(id) || version == null) 219 | { 220 | context.Response.StatusCode = 400; 221 | lock (Globals.ConsoleLock) 222 | Console.WriteLine($"Packages: return {context.Response.StatusCode}"); 223 | return; 224 | } 225 | 226 | version = version.Normalize(); 227 | 228 | string contentId = MasterData.FindContentId(id, version); 229 | 230 | if (contentId == null) 231 | { 232 | context.Response.StatusCode = 404; 233 | lock (Globals.ConsoleLock) 234 | Console.WriteLine($"Packages: return {context.Response.StatusCode}"); 235 | return; 236 | } 237 | 238 | var doc = new XElement(XmlElements.entry, 239 | new XAttribute(XmlElements.baze, XmlNamespaces.baze), 240 | new XAttribute(XmlElements.m, XmlNamespaces.m), 241 | new XAttribute(XmlElements.d, XmlNamespaces.d), 242 | new XAttribute(XmlElements.georss, XmlNamespaces.georss), 243 | new XAttribute(XmlElements.gml, XmlNamespaces.gml), 244 | new XElement(XmlElements.id, $"{context.Request.Scheme}://{context.Request.Host}{Segment}/Packages(Id='{id}',Version='{version}')"), 245 | new XElement( 246 | XmlElements.content, 247 | new XAttribute("type", "application/zip"), 248 | new XAttribute("src", $"{context.Request.Scheme}://{context.Request.Host}{Segment}/download/{contentId}") 249 | ), 250 | new XElement( 251 | XmlElements.m_properties, 252 | new XElement(XmlElements.d_id, id), 253 | new XElement(XmlElements.d_version, version) 254 | ) 255 | ); 256 | 257 | byte[] bytes = Encoding.UTF8.GetBytes(doc.ToString(SaveOptions.DisableFormatting)); 258 | 259 | context.Response.StatusCode = 200; 260 | context.Response.ContentType = "application/xml"; 261 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 262 | } 263 | 264 | private async Task FindPackage(HttpContext context) 265 | { 266 | var strings = context.Request.Query["id"]; 267 | if (strings.Count == 0) 268 | { 269 | context.Response.StatusCode = 400; 270 | lock (Globals.ConsoleLock) 271 | Console.WriteLine($"Find: return {context.Response.StatusCode}"); 272 | return; 273 | } 274 | 275 | var id = strings[0].Trim(Characters.SingleQuote); 276 | if (id.Length == 0) 277 | { 278 | context.Response.StatusCode = 400; 279 | lock (Globals.ConsoleLock) 280 | Console.WriteLine($"Find: return {context.Response.StatusCode}"); 281 | return; 282 | } 283 | 284 | VersionInfo[] versions = MasterData.Use(id, (packages, _, _id) => 285 | { 286 | foreach (var packageInfo in packages) 287 | { 288 | if (string.Equals(packageInfo.Id, _id, StringComparison.OrdinalIgnoreCase)) 289 | return packageInfo.Versions; 290 | } 291 | return null; 292 | }); 293 | 294 | if (versions == null) 295 | versions = Globals.EmptyVersionInfoArray; 296 | else 297 | { 298 | lock (Globals.ConsoleLock) 299 | { 300 | Console.WriteLine(); 301 | Console.WriteLine("--- Find -------------------------------------"); 302 | foreach (var v in versions) 303 | Console.WriteLine($"{id} - {v.Version}"); 304 | Console.WriteLine("----------------------------------------------"); 305 | Console.WriteLine(); 306 | } 307 | } 308 | 309 | var doc = new XElement( 310 | XmlElements.feed, 311 | new XAttribute(XmlElements.baze, XmlNamespaces.baze), 312 | new XAttribute(XmlElements.m, XmlNamespaces.m), 313 | new XAttribute(XmlElements.d, XmlNamespaces.d), 314 | new XAttribute(XmlElements.georss, XmlNamespaces.georss), 315 | new XAttribute(XmlElements.gml, XmlNamespaces.gml), 316 | new XElement(XmlElements.m_count, versions.Length.ToString()), 317 | versions.Select(x => 318 | new XElement( 319 | XmlElements.entry, 320 | new XElement(XmlElements.id, $"{context.Request.Scheme}://{context.Request.Host}{Segment}/Packages(Id='{id}',Version='{x.Version}')"), 321 | new XElement( 322 | XmlElements.content, 323 | new XAttribute("type", "application/zip"), 324 | new XAttribute("src", $"{context.Request.Scheme}://{context.Request.Host}{Segment}/download/{x.ContentId}") 325 | ), 326 | new XElement( 327 | XmlElements.m_properties, 328 | new XElement(XmlElements.d_id, id), 329 | new XElement(XmlElements.d_version, x.Version) 330 | ) 331 | ) 332 | ) 333 | ); 334 | 335 | versions = null; 336 | 337 | var bytes = Encoding.UTF8.GetBytes(doc.ToString(SaveOptions.DisableFormatting)); 338 | 339 | context.Response.StatusCode = 200; 340 | context.Response.ContentType = "application/xml"; 341 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /MinimalNugetServer/Version3RequestProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Newtonsoft.Json; 9 | 10 | namespace MinimalNugetServer 11 | { 12 | public class Version3RequestProcessor : RequestProcessorBase 13 | { 14 | public override PathString Segment { get; } = new PathString("/v3"); 15 | 16 | public override async Task ProcessRequest(HttpContext context) 17 | { 18 | PathString path; 19 | 20 | if (context.Request.Path.StartsWithSegments(Segment, out path) == false) 21 | return; 22 | 23 | PathString containerPath; 24 | 25 | string rootPath = path.Value.TrimStart(Characters.UrlPathSeparator).ToLower(); 26 | if (rootPath == string.Empty || rootPath == "index.json") 27 | { 28 | await ProcessRoot(context); 29 | return; 30 | } 31 | else if (path.StartsWithSegments("/container", out containerPath)) 32 | { 33 | await ProcessContainer(context, containerPath); 34 | return; 35 | } 36 | } 37 | 38 | private static readonly Version3IndexInfo[] indexIds = new Version3IndexInfo[] 39 | { 40 | new Version3IndexInfo("registration", "RegistrationsBaseUrl"), 41 | new Version3IndexInfo("container", "PackageBaseAddress/3.0.0"), 42 | new Version3IndexInfo("query", "SearchQueryService"), 43 | new Version3IndexInfo("autocomplete", "SearchAutocompleteService"), 44 | new Version3IndexInfo("catalog/index.json", "Catalog/3.0.0"), 45 | }; 46 | 47 | private async Task ProcessRoot(HttpContext context) 48 | { 49 | var urlPrefix = $"{context.Request.Scheme}://{context.Request.Host}{Segment}"; 50 | 51 | string json = JsonConvert.SerializeObject(new 52 | { 53 | version = "3.0.0", 54 | resources = indexIds 55 | .Select(x => x.ToSerializable(urlPrefix)) 56 | .ToArray() 57 | }, Formatting.None); 58 | 59 | context.Response.StatusCode = 200; 60 | context.Response.ContentType = "application/json"; 61 | await context.Response.WriteAsync(json, Encoding.UTF8); 62 | 63 | lock (Globals.ConsoleLock) 64 | Console.WriteLine($"Serving index"); 65 | } 66 | 67 | private async Task ProcessContainer(HttpContext context, PathString remainingPath) 68 | { 69 | lock (Globals.ConsoleLock) 70 | Console.WriteLine($"{nameof(remainingPath)}: {remainingPath}"); 71 | 72 | string[] pathParts = remainingPath.Value.Split(Characters.UrlPathSeparator, StringSplitOptions.RemoveEmptyEntries); 73 | 74 | if (pathParts.Length < 2) 75 | { 76 | context.Response.StatusCode = 400; 77 | lock (Globals.ConsoleLock) 78 | Console.WriteLine($"Container: return {context.Response.StatusCode}"); 79 | return; 80 | } 81 | 82 | string id = pathParts[0]; 83 | 84 | lock (Globals.ConsoleLock) 85 | Console.WriteLine($"{nameof(id)}: {id}"); 86 | 87 | int packageIndex = MasterData.FindPackageIndex(id); 88 | 89 | if (packageIndex < 0) 90 | { 91 | context.Response.StatusCode = 404; 92 | lock (Globals.ConsoleLock) 93 | Console.WriteLine($"Container: return {context.Response.StatusCode}"); 94 | return; 95 | } 96 | 97 | if (string.Equals(pathParts[1], "index.json", StringComparison.OrdinalIgnoreCase)) 98 | await ProcessContainerIndex(context, packageIndex); 99 | else 100 | { 101 | Version version; 102 | if (Version.TryParse(pathParts[1], out version)) 103 | await ProcessContainerVersion(context, packageIndex, version.Normalize()); 104 | } 105 | } 106 | 107 | private Task ProcessContainerIndex(HttpContext context, int packageIndex) 108 | { 109 | string[] allVersions = MasterData.Use(packageIndex, (packages, _, _packageIndex) => 110 | { 111 | return packages[_packageIndex].Versions 112 | .Select(x => x.Version.ToString()) 113 | .ToArray(); 114 | }); 115 | 116 | string json = JsonConvert.SerializeObject(new { versions = allVersions }, Formatting.None); 117 | 118 | context.Response.StatusCode = 200; 119 | context.Response.ContentType = "application/json"; 120 | return context.Response.WriteAsync(json, Encoding.UTF8); 121 | } 122 | 123 | private async Task ProcessContainerVersion(HttpContext context, int packageIndex, Version version) 124 | { 125 | lock (Globals.ConsoleLock) 126 | Console.WriteLine($"ProcessContainerVersion [version: {version}]"); 127 | 128 | byte[] content = MasterData.Use(packageIndex, version, (packages, contentFacade, _packageIndex, _version) => 129 | { 130 | VersionInfo[] versions = packages[_packageIndex].Versions; 131 | 132 | for (int i = 0; i < versions.Length; i++) 133 | { 134 | if (versions[i].Version == _version) 135 | { 136 | byte[] result; 137 | if (contentFacade.TryGetValue(versions[i].ContentId, out result)) 138 | return result; 139 | } 140 | } 141 | 142 | return null; 143 | }); 144 | 145 | if (content != null) 146 | { 147 | context.Response.StatusCode = 200; 148 | context.Response.ContentType = "application/octet-stream"; 149 | await context.Response.Body.WriteAsync(content, 0, content.Length); 150 | return; 151 | } 152 | 153 | context.Response.StatusCode = 404; 154 | lock (Globals.ConsoleLock) 155 | Console.WriteLine($"ContainerVersion: return {context.Response.StatusCode}"); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /MinimalNugetServer/configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "url": "http://*:4356" 4 | }, 5 | "nuget": { 6 | "packages": "/absolute/path/to/packages/folder", 7 | "makeReadonly": true 8 | }, 9 | "cache": { 10 | "type": "NoCacheLoadAll", 11 | "duration": -1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MinimalNugetServer/scripts/make_publishable.bat: -------------------------------------------------------------------------------- 1 | set CONFIGURATION=release 2 | set RUNTIME=netcoreapp2.0 3 | set OUTPUT=..\bin\%CONFIGURATION%\%RUNTIME%\publish\ 4 | 5 | pushd .. 6 | dotnet publish -c %CONFIGURATION% 7 | popd 8 | 9 | copy start.sh %OUTPUT% 10 | copy stahp.sh %OUTPUT% 11 | copy ..\configuration.json %OUTPUT% 12 | 13 | pause 14 | -------------------------------------------------------------------------------- /MinimalNugetServer/scripts/make_publishable.sh: -------------------------------------------------------------------------------- 1 | CONFIGURATION=release 2 | RUNTIME=netcoreapp2.0 3 | OUTPUT=../bin/${CONFIGURATION}/${RUNTIME}/publish/ 4 | pushd .. 5 | dotnet publish -c ${CONFIGURATION} 6 | popd 7 | cp start.sh ${OUTPUT} 8 | cp stahp.sh ${OUTPUT} 9 | cp ../configuration.json ${OUTPUT} 10 | chmod +x ${OUTPUT}start.sh 11 | chmod +x ${OUTPUT}stahp.sh 12 | -------------------------------------------------------------------------------- /MinimalNugetServer/scripts/stahp.sh: -------------------------------------------------------------------------------- 1 | kill -15 `cat run.pid` 2 | rm run.pid 3 | -------------------------------------------------------------------------------- /MinimalNugetServer/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #TZ=?/? 2 | #export TZ 3 | today=$(date +"%Y-%m-%d") 4 | now=$(date +"%H.%M.%S") 5 | mkdir -p logs 6 | mkdir -p logs/${today} 7 | dotnet MinimalNugetServer.dll "$@" >> "./logs/${today}/log_${now}.txt" 2>&1 & 8 | echo $! > run.pid 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This is a minimal implementation of a NuGet server. 4 | 5 | It supports API v2 for most of usages with `dotnet restore` and Visual Studio package manager (search packages, find package versions, etc...), and it also supports v3 with `dotnet restore` only, no Visual Studio support for v3 APIs. 6 | 7 | It basically works the same as if you were using a shared folder as package source. 8 | Thus, all `.nupkg` files in the root directory (and sub directories) are treated as a flat collection of package file. 9 | 10 | This server provides us a standard way to access our internally developped packages. 11 | 12 | # Note before getting started 13 | 14 | Since it is developped for .NET Core runtime, it is possible to build and run it virtually everywhere .NET Core runs. 15 | 16 | However, all the following instructions are written considering a Windows and Linux workstation to build and generate a publishable, and Linux machine as an hosting server. 17 | 18 | Also, please understand that this is a very simple and naive implementation, that could be suited for individuals or small organizations. 19 | 20 | See the section `Future works` at the bottom of this document for more details. 21 | 22 | # The problem it solves 23 | 24 | We need to self-host our internally developped packages, and prior this implementation, we were using a samba shared folder, hosted on a Linux machine. 25 | 26 | The main problem it solves is that we can finally get rid of the the samba shared folder, which has never worked correctly and has always been a pain in the butt, even with Windows clients, and now we have a very standard and very reliable way to access our packages through an HTTP interface. 27 | 28 | It seems for the moment the only available implementations of NuGet server is made for Windows only, but we need to host one on a Linux machine, so .NET Core is the perfect candidate runtime for that. 29 | 30 | # How to use it 31 | 32 | ## Build 33 | 34 | First, you must make sure you have .NET Core SDK 2.0. 35 | 36 | In a terminal, go to the directory that contains the `.csproj` file and run the `dotnet build` command. 37 | 38 | When you build for the first time, you may get the following error: 39 | 40 | error CS0103: The name 'GitCommitInfo' does not exist in the current context 41 | 42 | This is because a file is generated before compilation, and is not taken into account for the build. 43 | Just run `dotnet build` again and the it will finally compile correctly. 44 | 45 | ## Deploy 46 | 47 | ### For the impatients 48 | 49 | cd MinimalNugetServer/scripts 50 | 51 | # === on Windows === 52 | #make_publishable.bat 53 | 54 | # === on Linux === 55 | #./make_publishable.sh 56 | 57 | cd ../bin/release/netcoreapp2.0/publish/ 58 | # ===== edit the configuration.json file 59 | dotnet MinimalNugetServer.dll 60 | 61 | ### Detailed explanations 62 | 63 | For all `.sh` scripts you will need to run on Linux, remember that they must first be made executable by running the command `chmod +x