├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── db
└── .gitkeep
├── inc
├── config.php
├── core.php
├── db.php
└── feedwriter.php
├── nginx.conf.example
├── packagefiles
└── .gitkeep
└── public
├── count.php
├── delete.php
├── download.php
├── findByID.php
├── index.php
├── metadata.xml
├── push.php
├── search.php
└── updates.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = tab
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | db/*.sqlite3
2 | nginx.conf
3 | *.nupkg
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Daniel Lo Nigro
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Simple NuGet Server
2 | ===================
3 |
4 | A very simple NuGet server for my personal use, similar to
5 | [NuGet.Server](https://www.nuget.org/packages/NuGet.Server) but in PHP. Designed
6 | for scenarios where a single user (ie. a person or a build server) pushes
7 | packages.
8 |
9 | Features
10 | ========
11 | - Basic searching and listing of packages
12 | - Pushing via NuGet command line (NuGet.exe)
13 | - Simple set up
14 | - Stores data in SQLite or MySQL database
15 | - Uses a single API key (so not suitable for scenarios where multiple users
16 | need to be able to push packages)
17 |
18 | Setup
19 | =====
20 |
21 | For a Debian-based distro (including Ubuntu), installation is something along the lines of what I've written below. Installation using other Linux distributions will vary but it should be pretty similar.
22 |
23 | 1 - Ensure all dependencies are installed:
24 |
25 | - A web server (Nginx, Apache, Cherokee, etc.)
26 | - PHP 5.4+ or HHVM
27 | - SQLite, XML, and Zip extensions (bundled with HHVM, or `apt-get install php5-sqlite` for PHP 5, or `apt-get install php7.0-sqlite php7.0-xml php7.0-zip` for PHP 7)
28 |
29 | Note: If using Nginx, please make sure `ngx_http_dav_module` is installed. This is required to enable HTTP `PUT` support.
30 |
31 | 2 - Copy app to your server, you could do a Git clone on the server or add it as a Git submodule if adding to a site that's already managed via Git:
32 | ```bash
33 | cd /var/www
34 | git clone https://github.com/Daniel15/simple-nuget-server.git
35 | # Make db and packages directories writable
36 | chown www-data:www-data db packagefiles
37 | chmod 0770 db packagefiles
38 | ```
39 |
40 | 3 - Ensure you have a PHP 5.4+ or HHVM upstream configured. For example, in `/etc/nginx/conf.d/php.conf`:
41 | ```
42 | upstream php {
43 | server unix:/var/run/php7.2-fpm.sock;
44 | }
45 | ```
46 |
47 | 4 - Copy nginx.conf.example to `/etc/nginx/sites-available/nuget` and modify it for your environment:
48 | - Change `example.com` to your domain name
49 | - Change `/var/www/simple-nuget-server/` to the checkout directory
50 | - Change "php" to the name of a PHP upstream in your Nginx config, configured in step 3. This can be regular PHP 5.4+ or HHVM.
51 | - If hosting as part of another site, prefix everything with a subdirectory and combine the config with your existing site's config (see ReactJS.NET config at the end of this comment)
52 |
53 | 5 - Edit `inc/config.php` and change `ChangeThisKey` to a [randomly-generated string](https://www.random.org/strings/)
54 |
55 | 6 - Enable the site (if creating a new site)
56 | ```bash
57 | ln -s /etc/nginx/sites-available/nuget /etc/nginx/sites-enabled/nuget
58 | /etc/init.d/nginx reload
59 | ```
60 |
61 | 7 - Set the API key and test it out. I test using [nuget.exe](https://docs.nuget.org/consume/command-line-reference) (which is required for pushing)
62 | ```
63 | nuget.exe setApiKey -Source http://example.com/ ChangeThisKey
64 | nuget.exe push Foo.nupkg -Source http://example.com/
65 | ```
66 | (if using Mono, run `mono nuget.exe` instead)
67 |
68 | Licence
69 | =======
70 | (The MIT licence)
71 |
72 | Copyright (C) 2014 Daniel Lo Nigro (Daniel15)
73 |
74 | Permission is hereby granted, free of charge, to any person obtaining a copy of
75 | this software and associated documentation files (the "Software"), to deal in
76 | the Software without restriction, including without limitation the rights to
77 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
78 | of the Software, and to permit persons to whom the Software is furnished to do
79 | so, subject to the following conditions:
80 |
81 | The above copyright notice and this permission notice shall be included in all
82 | copies or substantial portions of the Software.
83 |
84 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
85 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
86 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
87 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
88 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
89 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
90 | SOFTWARE.
91 |
--------------------------------------------------------------------------------
/db/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Daniel15/simple-nuget-server/6a3f9b9ae3e9942e425c0a20430953f566314120/db/.gitkeep
--------------------------------------------------------------------------------
/inc/config.php:
--------------------------------------------------------------------------------
1 | getMessage());
18 | });
19 |
20 | // Make $_GET keys lower-case for improved NuGet client compatibility.
21 | $_GET = array_change_key_case($_GET, CASE_LOWER);
22 |
23 | /**
24 | * Ensures that the API key is valid.
25 | */
26 | function require_auth() {
27 | if (empty($_SERVER['HTTP_X_NUGET_APIKEY']) || $_SERVER['HTTP_X_NUGET_APIKEY'] != Config::$apiKey) {
28 | api_error('403', 'Invalid API key');
29 | }
30 | }
31 |
32 | /**
33 | * Gets the HTTP method used for the current request.
34 | */
35 | function request_method() {
36 | return !empty($_SERVER['HTTP_X_METHOD_OVERRIDE'])
37 | ? $_SERVER['HTTP_X_METHOD_OVERRIDE']
38 | : $_SERVER['REQUEST_METHOD'];
39 | }
40 |
41 | /**
42 | * Gets the file path for the specified package version. Throws an exception if
43 | * the package version does not exist.
44 | */
45 | function get_package_path($id, $version) {
46 | if (
47 | !DB::validateIdAndVersion($id, $version)
48 | // These should be caught by validateIdAndVersion, but better to be safe.
49 | || strpos($id, '/') !== false
50 | || strpos($version, '/') !== false
51 | ) {
52 | api_error('404', 'Package version not found');
53 | }
54 |
55 | // This is safe - These values have been validated via validateIdAndVersion above
56 | return '/packagefiles/' . $id . '/' . $version . '.nupkg';
57 | }
58 |
59 | /* Used to construct URIs */
60 | function url_scheme() {
61 | if ( (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443 || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
62 | return 'https://';
63 | } else {
64 | return 'http://';
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/inc/db.php:
--------------------------------------------------------------------------------
1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
13 | static::$isMysql = static::$conn->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql';
14 | static::createTables();
15 | }
16 |
17 | private static function createTables() {
18 | static::$conn->exec('
19 | CREATE TABLE IF NOT EXISTS packages (
20 | PackageId VARCHAR (256) PRIMARY KEY,
21 | Title VARCHAR (256),
22 | DownloadCount INTEGER NOT NULL DEFAULT 0,
23 | LatestVersion TEXT' . (static::$isMysql ? ',
24 | INDEX packages_DownloadCount (DownloadCount),
25 | INDEX packages_Title(Title)
26 | ' : '') . '
27 | );
28 | ' . (static::$isMysql ? '' : '
29 | CREATE INDEX IF NOT EXISTS packages_DownloadCount ON packages (DownloadCount);
30 | CREATE INDEX IF NOT EXISTS packages_Title ON packages (Title);
31 | ') . '
32 | CREATE TABLE IF NOT EXISTS versions (
33 | VersionId INTEGER AUTO_INCREMENT PRIMARY KEY,
34 | PackageId TEXT,
35 | Title TEXT,
36 | Description TEXT,
37 | Created INTEGER,
38 | Version VARCHAR (32),
39 | PackageHash TEXT,
40 | PackageHashAlgorithm TEXT,
41 | Dependencies TEXT,
42 | PackageSize INTEGER,
43 | ReleaseNotes TEXT,
44 | VersionDownloadCount INTEGER NOT NULL DEFAULT 0,
45 | Tags TEXT,
46 | LicenseUrl TEXT,
47 | ProjectUrl TEXT,
48 | IconUrl TEXT,
49 | Authors TEXT,
50 | Owners TEXT,
51 | RequireLicenseAcceptance BOOLEAN,
52 | Copyright TEXT,
53 | IsPrerelease BOOLEAN' . (static::$isMysql ? ',
54 | INDEX versions_Version (Version)
55 | ' : '') . '
56 | );
57 | ' . (static::$isMysql ? '' : '
58 | CREATE INDEX IF NOT EXISTS versions_Version ON versions (Version);
59 | ') . '
60 | ');
61 | }
62 |
63 | public static function countPackages() {
64 | return static::$conn->query('
65 | SELECT COUNT(PackageId) AS count FROM packages
66 | ')->fetchColumn();
67 | }
68 |
69 | public static function searchPackages($params) {
70 | $query_params = [];
71 | $where = '1=1';
72 | $pagination = '';
73 |
74 | // Defaults
75 | if (empty($params['orderBy'])) {
76 | $params['orderBy'] = Config::$defaultSortOrder;
77 | }
78 |
79 | if (!$params['includePrerelease']) {
80 | $where .= ' AND IsPrerelease = 0';
81 | }
82 | if (!empty($params['searchQuery'])) {
83 | $where .= ' AND
84 | (packages.Title LIKE :searchQuery
85 | OR packages.PackageId LIKE :searchQuery)';
86 | $query_params['searchQuery'] = '%' . $params['searchQuery'] . '%';
87 | }
88 | if (!empty($params['top'])) {
89 | // Have some attempt at checking valid values
90 | $int = intval($params['top']);
91 | if ($int > 0) {
92 | $pagination .= ' LIMIT ' . $int;
93 | }
94 | }
95 | if (!empty($params['offset'])) {
96 | $int = intval($params['offset']);
97 | if ($int > 0) {
98 | $pagination .= ' OFFSET ' . $int;
99 | }
100 | }
101 |
102 | switch ($params['filter']) {
103 | case '':
104 | break;
105 |
106 | case 'IsAbsoluteLatestVersion':
107 | case 'IsLatestVersion':
108 | $where .= ' AND versions.Version = packages.LatestVersion';
109 | break;
110 |
111 | default:
112 | throw new Exception('Unknown filter "' . $params['filter'] . '"');
113 | }
114 |
115 | $order = static::parseOrderBy($params['orderBy']);
116 |
117 | return static::doSearch($where, $order, $query_params, $pagination);
118 | }
119 |
120 | public static function packageUpdates($params) {
121 | $VERSION_SEPARATOR = '~~';
122 | $packages = [];
123 | $package_versions = [];
124 | foreach ($params['packages'] as $package => $version) {
125 | $packages[] = $package;
126 | $package_versions[] = $package . $VERSION_SEPARATOR . $version;
127 | }
128 |
129 | $query_params = [];
130 | // Where:
131 | // 1. Package is the latest version
132 | // 2. Package is in list of packages we want
133 | // 3. Package version is NOT the version we already have
134 | $where =
135 | 'versions.Version = packages.LatestVersion AND ' .
136 | static::buildInClause(
137 | 'packages.PackageId',
138 | $packages,
139 | $query_params
140 | ) . ' AND ' .
141 | static::buildInClause(
142 | 'packages.PackageId || "' . $VERSION_SEPARATOR . '" || versions.Version',
143 | $package_versions,
144 | $query_params,
145 | true
146 | );
147 |
148 | return static::doSearch(
149 | $where,
150 | 'packages.PackageId ASC',
151 | $query_params,
152 | ''
153 | );
154 | }
155 |
156 | /**
157 | * Finds a package by ID and version. If no version is passed, returns
158 | * all versions.
159 | */
160 | public static function findByID($id, $version = null) {
161 | $where = 'packages.PackageId = :id COLLATE NOCASE';
162 | $params = ['id' => $id];
163 | if (!empty($version)) {
164 | $where .= ' AND versions.Version = :version';
165 | $params['version'] = $version;
166 | }
167 | return static::doSearch($where, 'versions.Version DESC', $params, '');
168 | }
169 |
170 | private static function parseOrderBy($order_by) {
171 | $valid_sort_columns = [
172 | 'downloadcount' => 'packages.DownloadCount',
173 | 'id' => 'LOWER(packages.PackageId)',
174 | 'published' => 'versions.Created',
175 | ];
176 | $columns = explode(',', $order_by);
177 | $output = [];
178 |
179 | foreach ($columns as $column) {
180 | $direction = 'asc';
181 | $column = trim(strtolower($column));
182 | if (strpos($column, ' ') !== false) {
183 | $pieces = explode(' ', $column);
184 | $column = $pieces[0];
185 | $direction = $pieces[1];
186 | }
187 | if (!isset($valid_sort_columns[$column])) {
188 | throw new Exception('Unknown sort column "' . $column . '"');
189 | }
190 | if ($direction !== 'asc' && $direction !== 'desc') {
191 | throw new Exception('Unknown sort order "' . $direction .'"');
192 | }
193 | $output[] = $valid_sort_columns[$column] . ' ' . $direction;
194 | }
195 | return implode(', ', $output);
196 | }
197 |
198 | // *Assumes* $where and $order are sanitised!! This should be done at the
199 | // callsites!
200 | private static function doSearch($where, $order, $params, $pagination) {
201 | // TODO: Move this to a view
202 | $stmt = static::$conn->prepare('
203 | SELECT
204 | packages.PackageId, packages.DownloadCount,
205 | packages.LatestVersion,
206 | versions.Title, versions.Description,
207 | versions.Tags, versions.LicenseUrl, versions.ProjectUrl,
208 | versions.IconUrl, versions.Authors, versions.Owners,
209 | versions.RequireLicenseAcceptance, versions.Copyright,
210 | versions.Created, versions.Version, versions.PackageHash,
211 | versions.PackageHashAlgorithm, versions.PackageSize,
212 | versions.Dependencies, versions.ReleaseNotes,
213 | versions.VersionDownloadCount, versions.IsPrerelease
214 | FROM packages
215 | INNER JOIN versions ON packages.PackageId = versions.PackageId
216 | WHERE ' . $where . '
217 | ORDER BY ' . $order . '
218 | ' . $pagination . '
219 | ');
220 | $stmt->execute($params);
221 | return $stmt->fetchAll();
222 | }
223 |
224 | public static function validateIdAndVersion($id, $version) {
225 | $stmt = static::$conn->prepare('
226 | SELECT COUNT(1)
227 | FROM versions
228 | WHERE PackageId = :id AND Version = :version
229 | ');
230 | $stmt->execute([
231 | ':id' => $id,
232 | ':version' => $version,
233 | ]);
234 | return $stmt->fetchColumn() == 1;
235 | }
236 |
237 | public static function incrementDownloadCount($id, $version) {
238 | $stmt = static::$conn->prepare('
239 | UPDATE versions
240 | SET VersionDownloadCount = VersionDownloadCount + 1
241 | WHERE PackageId = :id AND Version = :version
242 | ');
243 | $stmt->execute([
244 | ':id' => $id,
245 | ':version' => $version,
246 | ]);
247 |
248 | // Denormalised since this isn't much of a perf issue and improves
249 | // query performance
250 | $stmt = static::$conn->prepare('
251 | UPDATE packages
252 | SET DownloadCount = DownloadCount + 1
253 | WHERE PackageId = :id
254 | ');
255 | $stmt->execute([
256 | ':id' => $id,
257 | ]);
258 | }
259 |
260 | public static function insertOrUpdatePackage($params) {
261 | // Upserts aren't standardised across DBMSes :(
262 | // Easiest thing here is to just do an insert followed by an update.
263 | $stmt = static::$conn->prepare('
264 | INSERT ' . (static::$isMysql ? '' : 'OR') . ' IGNORE INTO packages
265 | (PackageId, Title, LatestVersion)
266 | VALUES
267 | (:id, :title, :version)
268 | ');
269 | $stmt->execute($params);
270 |
271 | $stmt = static::$conn->prepare('
272 | UPDATE packages
273 | SET Title = :title, LatestVersion = :version
274 | WHERE PackageId = :id'
275 | );
276 | $stmt->execute($params);
277 | }
278 |
279 | public static function insertVersion($params) {
280 | $params[':Created'] = time();
281 | $params[':Dependencies'] = json_encode($params[':Dependencies']);
282 | $params[':IsPrerelease'] = empty($params[':IsPrerelease']) ? '0' : '1';
283 | $params[':RequireLicenseAcceptance'] =
284 | empty($params[':RequireLicenseAcceptance']) ? '0' : '1';
285 |
286 | $stmt = static::$conn->prepare('
287 | INSERT INTO versions (
288 | Authors, Copyright, Created, Dependencies, Description,
289 | IconUrl, IsPrerelease, LicenseUrl, Owners, PackageId,
290 | PackageHash, PackageHashAlgorithm, PackageSize, ProjectUrl,
291 | ReleaseNotes, RequireLicenseAcceptance, Tags, Title, Version
292 | )
293 | VALUES (
294 | :Authors, :Copyright, :Created, :Dependencies, :Description,
295 | :IconUrl, :IsPrerelease, :LicenseUrl, :Owners, :PackageId,
296 | :PackageHash, :PackageHashAlgorithm, :PackageSize, :ProjectUrl,
297 | :ReleaseNotes, :RequireLicenseAcceptance, :Tags, :Title, :Version
298 | )
299 | ');
300 | $stmt->execute($params);
301 | }
302 |
303 | /**
304 | * Deletes a particular package version. If there are no remaining versions
305 | * for this package, also deletes the package itself.
306 | */
307 | public static function deleteVersion($id, $version) {
308 | $stmt = static::$conn->prepare('
309 | DELETE FROM versions
310 | WHERE PackageId = :id AND Version = :version
311 | ');
312 | $stmt->execute([
313 | 'id' => $id,
314 | 'version' => $version,
315 | ]);
316 |
317 | // Check if any other version exist for this package
318 | $stmt = static::$conn->prepare('
319 | SELECT Version
320 | FROM versions
321 | WHERE PackageId = :id
322 | ORDER BY Created DESC
323 | LIMIT 1
324 | ');
325 | $stmt->execute([
326 | 'id' => $id,
327 | ]);
328 | $latestVersion = $stmt->fetchColumn();
329 |
330 | if (empty($latestVersion)) {
331 | // No other versions remaining, also delete the package
332 | $stmt = static::$conn->prepare('
333 | DELETE FROM packages
334 | WHERE PackageId = :id
335 | ');
336 | $stmt->execute([
337 | 'id' => $id,
338 | ]);
339 | } else {
340 | // There's still at least one version remaining, update the LatestVersion.
341 | $stmt = static::$conn->prepare('
342 | UPDATE packages
343 | SET LatestVersion = :latestVersion
344 | WHERE PackageId = :id
345 | ');
346 | $stmt->execute([
347 | 'id' => $id,
348 | 'latestVersion' => $latestVersion,
349 | ]);
350 | }
351 | }
352 |
353 | /**
354 | * Builds a parameterised IN(...) WHERE clause.
355 | *
356 | * @param $field Name of the field
357 | * @param $values Array of values to use in the IN clause
358 | * @param $params Hash to add query parameters to
359 | * @param $invert If true, do a "NOT IN" clause rather than an "IN" clause
360 | * @return There WHERE clause segment
361 | */
362 | private static function buildInClause($field, $values, &$params, $invert = false) {
363 | if (count($values) === 0) {
364 | return '1=1';
365 | }
366 |
367 | $i = 0;
368 | $placeholders = [];
369 | // Generate a unique prefix to use for all the query parameters
370 | $prefix = ':value' . mt_rand(0, 9999999);
371 | foreach ($values as $value) {
372 | $param_name = $prefix . $i;
373 | $params[$param_name] = $value;
374 | $placeholders[] = $param_name;
375 | $i++;
376 | }
377 | $clause = $field;
378 | if (!empty($invert)) {
379 | $clause = $clause . ' NOT';
380 | }
381 | $clause = $clause . ' IN (' . implode(', ', $placeholders) . ')';
382 | return $clause;
383 | }
384 | }
385 |
386 | DB::init();
387 |
--------------------------------------------------------------------------------
/inc/feedwriter.php:
--------------------------------------------------------------------------------
1 | feedID = $id;
8 | $this->baseURL =
9 | url_scheme() .
10 | $_SERVER['SERVER_NAME'] . ':' . $_SERVER['SERVER_PORT'] .
11 | rtrim(dirname($_SERVER['REQUEST_URI']), '/') . '/';
12 | }
13 |
14 | public function write(array $results) {
15 | $this->beginFeed();
16 | foreach ($results as $result) {
17 | $this->addEntry($result);
18 | }
19 | return $this->feed->asXML();
20 | }
21 |
22 | public function writeToOutput(array $results) {
23 | header('Content-Type: application/atom+xml; type=feed; charset=UTF-8');
24 | echo $this->write($results);
25 | }
26 |
27 | private function beginFeed() {
28 | $this->feed = simplexml_load_string(
29 | '
30 |
36 |
37 | ');
38 |
39 | $this->feed->addChild('id', $this->baseURL . $this->feedID);
40 | $this->addWithAttribs($this->feed, 'title', $this->feedID, ['type' => 'text']);
41 | $this->feed->addChild('updated', static::formatDate(time()));
42 | $this->addWithAttribs($this->feed, 'link', null, [
43 | 'rel' => 'self',
44 | 'title' => $this->feedID,
45 | 'href' => $this->feedID
46 | ]);
47 | }
48 |
49 | private function addEntry($row) {
50 | $entry_id = 'Packages(Id=\'' . $row['PackageId'] . '\',Version=\'' . $row['Version'] . '\')';
51 | $entry = $this->feed->addChild('entry');
52 | $entry->addChild('id', 'https://www.nuget.org/api/v2/' . $entry_id);
53 | $this->addWithAttribs($entry, 'category', null, [
54 | 'term' => 'NuGetGallery.V2FeedPackage',
55 | 'scheme' => 'http://schemas.microsoft.com/ado/2007/08/dataservices/scheme',
56 | ]);
57 | $this->addWithAttribs($entry, 'link', null, [
58 | 'rel' => 'edit',
59 | 'title' => 'V2FeedPackage',
60 | 'href' => $entry_id,
61 | ]);
62 | // Yes, this is correct. This "title" is actually the package ID
63 | // The actual title is in the metadata section. lolwat.
64 | $this->addWithAttribs($entry, 'title', $row['PackageId'], ['type' => 'text']);
65 | $this->addWithAttribs($entry, 'summary', null, ['type' => 'text']);
66 | $entry->addChild('updated', static::formatDate($row['Created']));
67 |
68 | $authors = $entry->addChild('author');
69 | $authors->addChild('name', $row['Authors']);
70 |
71 | $this->addWithAttribs($entry, 'link', null, [
72 | 'rel' => 'edit-media',
73 | 'title' => 'V2FeedPackage',
74 | 'href' => $entry_id . '/$value',
75 | ]);
76 | $this->addWithAttribs($entry, 'content', null, [
77 | 'type' => 'application/zip',
78 | 'src' => $this->baseURL . 'download/' . $row['PackageId'] . '/' . $row['Version'],
79 | ]);
80 | $this->addEntryMeta($entry, $row);
81 | }
82 |
83 | private function addEntryMeta($entry, $row) {
84 | $properties = $entry->addChild(
85 | 'properties',
86 | null,
87 | 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'
88 | );
89 |
90 | $meta = [
91 | 'Version' => $row['Version'],
92 | 'NormalizedVersion' => $row['Version'],
93 | 'Copyright' => $row['Copyright'],
94 | 'Created' => static::renderMetaDate($row['Created']),
95 | 'Dependencies' => $this->renderDependencies($row['Dependencies']),
96 | 'Description' => htmlspecialchars($row['Description']),
97 | 'DownloadCount' => ['value' => $row['DownloadCount'], 'type' => 'Edm.Int32'],
98 | 'GalleryDetailsUrl' => $this->baseURL . 'details/' . $row['PackageId'] . '/' . $row['Version'],
99 | 'IconUrl' => htmlspecialchars($row['IconUrl']),
100 | 'IsLatestVersion' => static::renderMetaBoolean($row['LatestVersion'] === $row['Version']),
101 | 'IsAbsoluteLatestVersion' => static::renderMetaBoolean($row['LatestVersion'] === $row['Version']),
102 | 'IsPrerelease' => static::renderMetaBoolean($row['IsPrerelease']),
103 | 'Language' => null,
104 | 'Published' => static::renderMetaDate($row['Created']),
105 | 'PackageHash' => $row['PackageHash'],
106 | 'PackageHashAlgorithm' => $row['PackageHashAlgorithm'],
107 | 'PackageSize' => ['value' => $row['PackageSize'], 'type' => 'Edm.Int64'],
108 | 'ProjectUrl' => $row['ProjectUrl'],
109 | 'ReportAbuseUrl' => '',
110 | 'ReleaseNotes' => htmlspecialchars($row['ReleaseNotes']),
111 | 'RequireLicenseAcceptance' => static::renderMetaBoolean($row['RequireLicenseAcceptance']),
112 | 'Summary' => null,
113 | 'Tags' => $row['Tags'],
114 | 'Title' => $row['Title'],
115 | 'VersionDownloadCount' => ['value' => $row['VersionDownloadCount'], 'type' => 'Edm.Int32'],
116 | 'MinClientVersion' => '',
117 | 'LastEdited' => ['value' => null, 'type' => 'Edm.DateTime'],
118 | 'LicenseUrl' => $row['LicenseUrl'],
119 | 'LicenseNames' => '',
120 | 'LicenseReportUrl' => '',
121 | ];
122 |
123 | foreach ($meta as $name => $data) {
124 | if (is_array($data)) {
125 | $value = $data['value'];
126 | $type = $data['type'];
127 | } else {
128 | $value = $data;
129 | $type = null;
130 | }
131 |
132 | $this->addMeta($properties, $name, $value, $type);
133 | }
134 |
135 | }
136 |
137 | private static function renderMetaDate($date) {
138 | return [
139 | 'value' => static::formatDate($date),
140 | 'type' => 'Edm.DateTime'
141 | ];
142 | }
143 |
144 | private static function renderMetaBoolean($value) {
145 | return [
146 | 'value' => $value ? 'true' : 'false',
147 | 'type' => 'Edm.Boolean'
148 | ];
149 | }
150 |
151 | private static function formatDate($date) {
152 | return gmdate('Y-m-d\TH:i:s\Z', $date);
153 | }
154 |
155 | private function renderDependencies($raw) {
156 | if (!$raw) {
157 | return '';
158 | }
159 |
160 | $data = json_decode($raw);
161 | if (!$data) {
162 | return '';
163 | }
164 |
165 | $output = [];
166 | // Hax: Previous versions used an associative array of id => version for the
167 | // dependencies, but newer versions use a 'real' array. Determine if we're
168 | // using the old format or the new format.
169 | if (is_array($data)) {
170 | foreach ($data as $dependency) {
171 | $formatted_dependency =
172 | $dependency->id . ':' .
173 | $dependency->version . ':';
174 | if (!empty($dependency->framework)) {
175 | $formatted_dependency .= $this->formatTargetFramework($dependency->framework);
176 | }
177 | $output[] = $formatted_dependency;
178 | }
179 | } else {
180 | // Legacy format
181 | foreach ($data as $id => $version) {
182 | $output[] = $id . ':' . $version . ':';
183 | }
184 | }
185 | return implode('|', $output);
186 | }
187 |
188 | /**
189 | * Formats a raw target framework from a NuSpec into the format used in the
190 | * packages feed (eg. "DNX4.5.1" -> "dnx451", "DNXCore5.0" -> "dnxcore50").
191 | */
192 | private function formatTargetFramework($framework) {
193 | return strtolower(preg_replace('/[^A-Z0-9]/i', '', $framework));
194 | }
195 |
196 | private function addWithAttribs($entry, $name, $value, $attributes) {
197 | $node = $entry->addChild($name, $value);
198 | foreach ($attributes as $attrib_name => $attrib_value) {
199 | $node->addAttribute($attrib_name, $attrib_value);
200 | }
201 | }
202 |
203 | private function addMeta($entry, $name, $value, $type = null) {
204 | $node = $entry->addChild(
205 | $name,
206 | $value,
207 | 'http://schemas.microsoft.com/ado/2007/08/dataservices'
208 | );
209 | if ($type) {
210 | $node->addAttribute(
211 | 'm:type',
212 | $type,
213 | 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'
214 | );
215 | }
216 | if ($value === null) {
217 | $node->addAttribute(
218 | 'm:null',
219 | 'true',
220 | 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'
221 | );
222 | }
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/nginx.conf.example:
--------------------------------------------------------------------------------
1 | # Example of an Nginx configuration for Simple NuGet Server
2 |
3 | server {
4 | server_name example.com;
5 | root /var/www/simple-nuget-server/public/;
6 |
7 | rewrite ^/$ /index.php;
8 | rewrite ^/\$metadata$ /metadata.xml;
9 | rewrite ^/Search\(\)/\$count$ /count.php;
10 | rewrite ^/Search\(\)$ /search.php;
11 | rewrite ^/Packages\(\)$ /search.php;
12 | rewrite ^/Packages\(Id='([^']+)',Version='([^']+)'\)$ /findByID.php?id=$1&version=$2;
13 | rewrite ^/GetUpdates\(\)$ /updates.php;
14 | rewrite ^/FindPackagesById\(\)$ /findByID.php;
15 | # NuGet.exe sometimes uses two slashes (//download/blah)
16 | rewrite ^//?download/([^/]+)/([^/]+)$ /download.php?id=$1&version=$2;
17 | rewrite ^/([^/]+)/([^/]+)$ /delete.php?id=$1&version=$2;
18 |
19 | # NuGet.exe adds /api/v2/ to URL when the server is at the root
20 | rewrite ^/api/v2/package/$ /index.php;
21 | rewrite ^/api/v2/package/([^/]+)/([^/]+)$ /delete.php?id=$1&version=$2;
22 |
23 | location ~ \.php$ {
24 | include fastcgi_params;
25 | fastcgi_pass php;
26 | }
27 |
28 | location = /index.php {
29 | dav_methods PUT DELETE;
30 | include fastcgi_params;
31 | fastcgi_pass php;
32 |
33 | # PHP doesn't parse request body for PUT requests, so fake a POST.
34 | fastcgi_param REQUEST_METHOD POST;
35 | fastcgi_param HTTP_X_METHOD_OVERRIDE $request_method;
36 | }
37 |
38 | # Used with X-Accel-Redirect
39 | location /packagefiles {
40 | internal;
41 | root /var/www/simple-nuget-server/;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packagefiles/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Daniel15/simple-nuget-server/6a3f9b9ae3e9942e425c0a20430953f566314120/packagefiles/.gitkeep
--------------------------------------------------------------------------------
/public/count.php:
--------------------------------------------------------------------------------
1 | writeToOutput($results);
14 |
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | ";
13 | ?>
14 |
18 |
19 | Default
20 |
21 | Packages
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/public/metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/public/push.php:
--------------------------------------------------------------------------------
1 | open($upload_filename);
14 | $nuspec_index = false;
15 | for ($i = 0; $i < $package_zip->numFiles; $i++) {
16 | if (substr($package_zip->getNameIndex($i), -7) === '.nuspec') {
17 | $nuspec_index = $i;
18 | break;
19 | }
20 | }
21 | if ($nuspec_index === false) {
22 | api_error('400', 'NuSpec file not found in package');
23 | }
24 | $nuspec_string = $package_zip->getFromIndex($nuspec_index);
25 | $nuspec = simplexml_load_string($nuspec_string);
26 |
27 | if (!$nuspec->metadata->id || !$nuspec->metadata->version) {
28 | api_error('400', 'ID or version is missing');
29 | }
30 |
31 | $id = (string)$nuspec->metadata->id;
32 | $version = (string)$nuspec->metadata->version;
33 | $valid_id = '/^[A-Z0-9\.\~\+\_\-]+$/i';
34 | if (!preg_match($valid_id, $id) || !preg_match($valid_id, $version)) {
35 | api_error('400', 'Invalid ID or version');
36 | }
37 |
38 | if (DB::validateIdAndVersion($id, $version)) {
39 | api_error('409', 'This package version already exists');
40 | }
41 |
42 | $hash = base64_encode(hash_file('sha512', $upload_filename, true));
43 | $filesize = filesize($_FILES['package']['tmp_name']);
44 | $dependencies = [];
45 |
46 |
47 | if ($nuspec->metadata->dependencies) {
48 | if ($nuspec->metadata->dependencies->dependency) {
49 | // Dependencies that are not specific to any framework
50 | foreach ($nuspec->metadata->dependencies->dependency as $dependency) {
51 | $dependencies[] = [
52 | 'framework' => null,
53 | 'id' => (string)$dependency['id'],
54 | 'version' => (string)$dependency['version']
55 | ];
56 | }
57 | }
58 |
59 | if ($nuspec->metadata->dependencies->group) {
60 | // Dependencies that are specific to a particular framework
61 | foreach ($nuspec->metadata->dependencies->group as $group) {
62 | if ($group->dependency) {
63 | foreach ($group->dependency as $dependency) {
64 | $dependencies[] = [
65 | 'framework' => (string)$group['targetFramework'],
66 | 'id' => (string)$dependency['id'],
67 | 'version' => (string)$dependency['version']
68 | ];
69 | }
70 | } else {
71 | // Group doesn't have any dependencies but we still need to save the framework
72 | $dependencies[] = [
73 | 'framework' => (string)$group['targetFramework'],
74 | 'id' => '',
75 | 'version' => ''
76 | ];
77 | }
78 | }
79 | }
80 | }
81 |
82 | // Move package into place.
83 | $dir = Config::$packageDir . $id . DIRECTORY_SEPARATOR;
84 | $path = $dir . $version . '.nupkg';
85 |
86 | if (!file_exists($dir)) {
87 | mkdir($dir, /* mode */ 0755, /* recursive */ true);
88 | }
89 | if (!move_uploaded_file($upload_filename, $path)) {
90 | api_error('500', 'Could not save file');
91 | }
92 |
93 | // Update database
94 | DB::insertOrUpdatePackage([
95 | ':id' => $id,
96 | ':title' => $nuspec->metadata->title,
97 | ':version' => $version
98 | ]);
99 | DB::insertVersion([
100 | ':Authors' => $nuspec->metadata->authors,
101 | ':Copyright' => $nuspec->metadata->copyright,
102 | ':Dependencies' => $dependencies,
103 | ':Description' => $nuspec->metadata->description,
104 | ':PackageHash' => $hash,
105 | ':PackageHashAlgorithm' => 'SHA512',
106 | ':PackageSize' => $filesize,
107 | ':IconUrl' => $nuspec->metadata->iconUrl,
108 | ':IsPrerelease' => strpos($version, '-') !== false,
109 | ':LicenseUrl' => $nuspec->metadata->licenseUrl,
110 | ':Owners' => $nuspec->metadata->owners,
111 | ':PackageId' => $id,
112 | ':ProjectUrl' => $nuspec->metadata->projectUrl,
113 | ':ReleaseNotes' => $nuspec->metadata->releaseNotes,
114 | ':RequireLicenseAcceptance' => $nuspec->metadata->requireLicenseAcceptance === 'true',
115 | ':Tags' => $nuspec->metadata->tags,
116 | ':Title' => $nuspec->metadata->title,
117 | ':Version' => $version,
118 | ]);
119 |
120 | // All done!
121 | header('HTTP/1.1 201 Created');
122 |
--------------------------------------------------------------------------------
/public/search.php:
--------------------------------------------------------------------------------
1 | !empty($_GET['includeprerelease']),
7 | 'orderBy' => isset($_GET['$orderby']) ? $_GET['$orderby'] : '',
8 | 'filter' => isset($_GET['$filter']) ? $_GET['$filter'] : '',
9 | 'searchQuery' => isset($_GET['searchterm']) ? trim($_GET['searchterm'], '\'') : '',
10 | 'top' => isset($_GET['$top']) ? $_GET['$top'] : '',
11 | 'offset' => isset($_GET['$skip']) ? $_GET['$skip'] : ''
12 | ]);
13 | $feed = new FeedWriter('Search');
14 | $feed->writeToOutput($results);
15 |
--------------------------------------------------------------------------------
/public/updates.php:
--------------------------------------------------------------------------------
1 | !empty($_GET['includeprerelease']),
11 | 'packages' => $package_to_version,
12 | ]);
13 | $feed = new FeedWriter('GetUpdates');
14 | $feed->writeToOutput($results);
15 |
--------------------------------------------------------------------------------