@@ -26,15 +26,21 @@ internal class Job : JobBase
2626 public static readonly long DefaultMaxAllowedManifestBytes = 10 /* Mb */ * 1024 /* Kb */ * 1024 ; /* b */
2727
2828 public static readonly string GetEditsBaseSql = @"
29- SELECT pr.Id, p.NormalizedVersion AS Version, p.Hash, e.*
29+ SELECT pr.Id, p.NormalizedVersion AS Version, p.Hash, p.HasReadMe, e.*
3030 FROM PackageEdits e
3131 INNER JOIN Packages p ON p.[Key] = e.PackageKey
3232 INNER JOIN PackageRegistrations pr ON pr.[Key] = p.PackageRegistrationKey" ;
3333
3434 public const string DefaultSourceContainerName = "packages" ;
3535 public const string DefaultBackupContainerName = "package-backups" ;
36+ public const string DefaultReadMeContainerName = "readmes" ;
3637 public const int DefaultMaxRetryCount = 10 ;
3738
39+ private const string ReadMeChanged = "changed" ;
40+ private const string ReadMeDeleted = "deleted" ;
41+ private const string MarkdownExtension = "md" ;
42+ private const string HtmlExtension = "html" ;
43+
3844 /// <summary>
3945 /// Gets or sets an Azure Storage Uri referring to a container to use as the source for package blobs
4046 /// </summary>
@@ -52,18 +58,31 @@ FROM PackageEdits e
5258 public CloudStorageAccount Backups { get ; set ; }
5359 public string BackupsContainerName { get ; set ; }
5460
61+ /// <summary>
62+ /// Gets or sets an Azure Storage Uri referring to a container to use as the storage for ReadMes
63+ /// </summary>
64+ public string ReadMeContainerName { get ; set ; }
65+
5566 /// <summary>
5667 /// Gets or sets the maximum number of tries that are allowed before considering an edit failed.
5768 /// </summary>
5869 public int ? MaxTryCount { get ; set ; }
5970
6071 public long ? MaxAllowedManifestBytes { get ; set ; }
6172
62- protected CloudBlobContainer SourceContainer { get ; set ; }
63- protected CloudBlobContainer BackupsContainer { get ; set ; }
73+ private CloudBlobContainer SourceContainer { get ; set ; }
74+ private CloudBlobContainer BackupsContainer { get ; set ; }
75+ private CloudBlobContainer ReadMeContainer { get ; set ; }
6476 protected long MaxManifestSize { get ; set ; }
6577 private ILogger _logger ;
6678
79+ private class ReadMeBlobs
80+ {
81+ public CloudBlockBlob activeBlob = null ;
82+ public CloudBlockBlob pendingBlob = null ;
83+ public CloudBlockBlob activeSnapshot = null ;
84+ }
85+
6786 public override bool Init ( IDictionary < string , string > jobArgsDictionary )
6887 {
6988 try
@@ -86,12 +105,13 @@ public override bool Init(IDictionary<string, string> jobArgsDictionary)
86105 JobConfigurationManager . GetArgument ( jobArgsDictionary , JobArgumentNames . SourceStorage ) ) ;
87106 Backups = CloudStorageAccount . Parse (
88107 JobConfigurationManager . GetArgument ( jobArgsDictionary , JobArgumentNames . BackupStorage ) ) ;
89-
90108 SourceContainerName = JobConfigurationManager . TryGetArgument ( jobArgsDictionary , JobArgumentNames . SourceContainerName ) ?? DefaultSourceContainerName ;
91109 BackupsContainerName = JobConfigurationManager . TryGetArgument ( jobArgsDictionary , JobArgumentNames . BackupContainerName ) ?? DefaultBackupContainerName ;
110+ ReadMeContainerName = JobConfigurationManager . TryGetArgument ( jobArgsDictionary , JobArgumentNames . ReadMeContainerName ) ?? DefaultReadMeContainerName ;
92111
93112 SourceContainer = Source . CreateCloudBlobClient ( ) . GetContainerReference ( SourceContainerName ) ;
94113 BackupsContainer = Backups . CreateCloudBlobClient ( ) . GetContainerReference ( BackupsContainerName ) ;
114+ ReadMeContainer = Source . CreateCloudBlobClient ( ) . GetContainerReference ( ReadMeContainerName ) ;
95115
96116 MaxTryCount = DefaultMaxRetryCount ;
97117 }
@@ -154,13 +174,14 @@ public override async Task<bool> Run()
154174 await UpdatePackageEditDbWithError ( exception , edit . Key ) ;
155175 }
156176 }
157-
158177 return true ;
159178 }
160179
161180 private async Task ApplyEdit ( PackageEdit edit )
162181 {
163182 string originalPath = null ;
183+ string originalReadMeMDPath = null ;
184+ string originalReadMeHTMLPath = null ;
164185
165186 try
166187 {
@@ -172,6 +193,8 @@ private async Task ApplyEdit(PackageEdit edit)
172193 }
173194
174195 originalPath = Path . Combine ( directory , "original.nupkg" ) ;
196+ Trace . TraceInformation ( $ "Downloading nupkg to { originalPath } ") ;
197+
175198 var sourceBlob = SourceContainer . GetBlockBlobReference (
176199 StorageHelpers . GetPackageBlobName ( edit . Id , edit . Version ) ) ;
177200 Trace . TraceInformation ( $ "Name is { sourceBlob . Name } , storage uri is { sourceBlob . StorageUri } ") ;
@@ -223,8 +246,12 @@ private async Task ApplyEdit(PackageEdit edit)
223246 hash = Convert . ToBase64String (
224247 hashAlgorithm . ComputeHash ( originalStream ) ) ;
225248 }
249+
250+ // ReadMe update
251+ var readMeMDBlob = await UpdateReadMeAsync ( edit , directory , originalReadMeMDPath , MarkdownExtension ) ;
252+
253+ var readMeHTMLBlob = await UpdateReadMeAsync ( edit , directory , originalReadMeHTMLPath , HtmlExtension ) ;
226254
227- // Update the database
228255 try
229256 {
230257 Trace . TraceInformation ( $ "Updating package record for { edit . Id } { edit . Version } ") ;
@@ -234,20 +261,52 @@ private async Task ApplyEdit(PackageEdit edit)
234261 catch ( Exception exception )
235262 {
236263 // Error occurred while updaing database, roll back the blob to the snapshot
237- // Can't do "await" in a catch block, but this should be pretty quick since it just starts the copy
238264 Trace . TraceError ( $ "Failed to update database! { exception } ") ;
239265 Trace . TraceWarning (
240266 $ "Rolling back updated blob for { edit . Id } { edit . Version } . Copying snapshot { sourceSnapshot . Uri . AbsoluteUri } to { sourceBlob . Uri . AbsoluteUri } ") ;
241- sourceBlob . StartCopy ( sourceSnapshot ) ;
267+ await sourceBlob . StartCopyAsync ( sourceSnapshot ) ;
268+ while ( sourceBlob . CopyState . Status != CopyStatus . Success )
269+ {
270+ await Task . Delay ( 1000 ) ;
271+ }
242272 Trace . TraceWarning (
243273 $ "Rolled back updated blob for { edit . Id } { edit . Version } . Copying snapshot { sourceSnapshot . Uri . AbsoluteUri } to { sourceBlob . Uri . AbsoluteUri } ") ;
244274
275+ await RollBackReadMeAsync ( edit , directory , originalReadMeMDPath , readMeMDBlob . activeSnapshot , readMeMDBlob . activeBlob ) ;
276+ await RollBackReadMeAsync ( edit , directory , originalReadMeHTMLPath , readMeHTMLBlob . activeSnapshot , readMeHTMLBlob . activeBlob ) ;
277+
245278 throw ;
246279 }
247280
281+ if ( edit . ReadMeState == ReadMeChanged )
282+ {
283+ // Delete pending ReadMes
284+ Trace . TraceInformation ( $ "Deleting pending ReadMe for { edit . Id } { edit . Version } from { readMeMDBlob . pendingBlob . Uri . AbsoluteUri } ") ;
285+ await readMeMDBlob . pendingBlob . DeleteIfExistsAsync ( ) ;
286+ Trace . TraceInformation ( $ "Deleted pending ReadMe for { edit . Id } { edit . Version } from { readMeMDBlob . pendingBlob . Uri . AbsoluteUri } ") ;
287+
288+ Trace . TraceInformation ( $ "Deleting pending ReadMe for { edit . Id } { edit . Version } from { readMeHTMLBlob . pendingBlob . Uri . AbsoluteUri } ") ;
289+ await readMeHTMLBlob . pendingBlob . DeleteIfExistsAsync ( ) ;
290+ Trace . TraceInformation ( $ "Deleted pending ReadMe for { edit . Id } { edit . Version } from { readMeHTMLBlob . pendingBlob . Uri . AbsoluteUri } ") ;
291+ }
292+
248293 Trace . TraceInformation ( "Deleting snapshot blob {2} for {0} {1}." , edit . Id , edit . Version , sourceSnapshot . Uri . AbsoluteUri ) ;
249294 await sourceSnapshot . DeleteAsync ( ) ;
250295 Trace . TraceInformation ( "Deleted snapshot blob {2} for {0} {1}." , edit . Id , edit . Version , sourceSnapshot . Uri . AbsoluteUri ) ;
296+
297+ if ( readMeMDBlob . activeSnapshot != null )
298+ {
299+ Trace . TraceInformation ( "Deleting snapshot ReadMe {2} for {0} {1}." , edit . Id , edit . Version , readMeMDBlob . activeSnapshot . Uri . AbsoluteUri ) ;
300+ await readMeMDBlob . activeSnapshot . DeleteAsync ( ) ;
301+ Trace . TraceInformation ( "Deleted snapshot ReadMe{2} for {0} {1}." , edit . Id , edit . Version , readMeMDBlob . activeSnapshot . Uri . AbsoluteUri ) ;
302+ }
303+
304+ if ( readMeHTMLBlob . activeSnapshot != null )
305+ {
306+ Trace . TraceInformation ( "Deleting snapshot ReadMe {2} for {0} {1}." , edit . Id , edit . Version , readMeHTMLBlob . activeSnapshot . Uri . AbsoluteUri ) ;
307+ await readMeHTMLBlob . activeSnapshot . DeleteAsync ( ) ;
308+ Trace . TraceInformation ( "Deleted snapshot ReadMe {2} for {0} {1}." , edit . Id , edit . Version , readMeHTMLBlob . activeSnapshot . Uri . AbsoluteUri ) ;
309+ }
251310 }
252311 finally
253312 {
@@ -277,6 +336,8 @@ private async Task UpdateDatabaseWithEdit(PackageEdit edit, string hash, long si
277336 edit . IconUrl ,
278337 edit . LicenseUrl ,
279338 edit . ProjectUrl ,
339+ edit . RepositoryUrl ,
340+ edit . ReadMeState ,
280341 edit . ReleaseNotes ,
281342 edit . RequiresLicenseAcceptance ,
282343 edit . Summary ,
@@ -290,6 +351,20 @@ private async Task UpdateDatabaseWithEdit(PackageEdit edit, string hash, long si
290351 HashAlgorithm = HashAlgorithmName
291352 } ) ;
292353
354+ // Update parameters with new HasReadMe value
355+ var HasReadMeSQL = string . Empty ;
356+ if ( edit . ReadMeState == ReadMeChanged )
357+ {
358+ parameters . Add ( "HasReadMe" , 1 ) ;
359+ HasReadMeSQL = ", HasReadMe = @HasReadMe" ;
360+
361+ }
362+ else if ( edit . ReadMeState == ReadMeDeleted )
363+ {
364+ parameters . Add ( "HasReadMe" , 0 ) ;
365+ HasReadMeSQL = ", HasReadMe = @HasReadMe" ;
366+ }
367+
293368 // Prep SQL for merging in authors
294369 var loadAuthorsSql = new StringBuilder ( ) ;
295370 var authors = edit . Authors . Split ( ',' ) ;
@@ -299,7 +374,7 @@ private async Task UpdateDatabaseWithEdit(PackageEdit edit, string hash, long si
299374 parameters . Add ( "Author" + i , authors [ i ] ) ;
300375 }
301376
302- await connection . QueryAsync < int > ( @"
377+ await connection . QueryAsync < int > ( $ @ "
303378 BEGIN TRANSACTION
304379 -- Form a comma-separated list of authors
305380 DECLARE @existingAuthors nvarchar(MAX)
@@ -327,7 +402,8 @@ INSERT INTO [PackageHistories]
327402 HashAlgorithm,
328403 PackageFileSize,
329404 LastUpdated,
330- Published
405+ Published,
406+ RepositoryUrl
331407 FROM [Packages]
332408 WHERE [Key] = @PackageKey
333409
@@ -349,7 +425,9 @@ UPDATE [Packages]
349425 Hash = @Hash,
350426 HashAlgorithm = @HashAlgorithm,
351427 PackageFileSize = @PackageFileSize,
352- FlattenedAuthors = @Authors
428+ FlattenedAuthors = @Authors,
429+ RepositoryUrl = @RepositoryUrl
430+ { HasReadMeSQL }
353431 WHERE [Key] = @PackageKey
354432
355433 -- Update Authors
@@ -389,7 +467,123 @@ UPDATE PackageEdits
389467 {
390468 Trace . TraceError ( $ "Error updating the package edit database with error! { ex } ") ;
391469 }
470+ }
471+
472+ private async Task < ReadMeBlobs > UpdateReadMeAsync ( PackageEdit edit ,
473+ string directory , string originalReadMePath , string readMeExtension )
474+ {
475+ ReadMeBlobs currentBlob = new ReadMeBlobs ( ) ;
392476
477+ // ReadMeState of null means unmodified
478+ if ( edit . ReadMeState == null )
479+ {
480+ return currentBlob ;
481+ }
482+
483+ originalReadMePath = Path . Combine ( directory , "readme" + readMeExtension ) ;
484+ Trace . TraceInformation ( $ "Attempting to save ReadMe at { originalReadMePath } ") ;
485+
486+ currentBlob . activeBlob = ReadMeContainer . GetBlockBlobReference (
487+ StorageHelpers . GetActiveReadMeBlobNamePath ( edit . Id , edit . Version , readMeExtension ) ) ;
488+ Trace . TraceInformation ( $ "Found active ReadMe { currentBlob . activeBlob . Name } at storage URI { currentBlob . activeBlob . StorageUri } ") ;
489+
490+ // Update ReadMe in blob storage
491+ if ( edit . ReadMeState == ReadMeChanged )
492+ {
493+ // Do the blob update
494+ try
495+ {
496+ currentBlob . pendingBlob = ReadMeContainer . GetBlockBlobReference (
497+ StorageHelpers . GetPendingReadMeBlobNamePath ( edit . Id , edit . Version , readMeExtension ) ) ;
498+ Trace . TraceInformation ( $ "Found pending ReadMe { currentBlob . pendingBlob . Name } at storage URI { currentBlob . pendingBlob . StorageUri } ") ;
499+
500+ if ( edit . HasReadMe )
501+ {
502+ // Snapshot the original blob if it exists (so if it's an edit, not an upload)
503+ Trace . TraceInformation ( $ "Snapshotting original blob for { edit . Id } { edit . Version } ({ currentBlob . activeBlob . Uri . AbsoluteUri } ).") ;
504+ currentBlob . activeSnapshot = await currentBlob . activeBlob . CreateSnapshotAsync ( ) ;
505+ Trace . TraceInformation ( $ "Snapshotted original blob for { edit . Id } { edit . Version } ({ currentBlob . activeBlob . Uri . AbsoluteUri } ).") ;
506+ }
507+
508+ // Download pending ReadMe
509+ Trace . TraceInformation ( $ "Downloading new ReadMe for { edit . Id } { edit . Version } ") ;
510+ await currentBlob . pendingBlob . DownloadToFileAsync ( originalReadMePath , FileMode . Create ) ;
511+ Trace . TraceInformation ( $ "Downloaded new ReadMe for { edit . Id } { edit . Version } ") ;
512+
513+ // Upload pending ReadMe to active
514+ Trace . TraceInformation ( $ "Uploading new ReadMe for { edit . Id } { edit . Version } to { currentBlob . activeBlob . Uri . AbsoluteUri } ") ;
515+ await currentBlob . activeBlob . UploadFromFileAsync ( originalReadMePath ) ;
516+ Trace . TraceInformation ( $ "Uploaded new ReadMe for { edit . Id } { edit . Version } to { currentBlob . activeBlob . Uri . AbsoluteUri } ") ;
517+ }
518+ finally
519+ {
520+ if ( ! string . IsNullOrEmpty ( originalReadMePath ) && File . Exists ( originalReadMePath ) )
521+ {
522+ File . Delete ( originalReadMePath ) ;
523+ }
524+ }
525+ }
526+ // Delete ReadMe in blob storage
527+ else if ( edit . ReadMeState == ReadMeDeleted )
528+ {
529+ // Download active ReadMe
530+ Trace . TraceInformation ( $ "Downloading old ReadMe for { edit . Id } { edit . Version } ") ;
531+ await currentBlob . activeBlob . DownloadToFileAsync ( originalReadMePath , FileMode . Create ) ;
532+ Trace . TraceInformation ( $ "Downloaded old ReadMe for { edit . Id } { edit . Version } ") ;
533+
534+ // Delete active ReadMe
535+ Trace . TraceInformation ( $ "Deleting ReadMe of { edit . Id } { edit . Version } from { currentBlob . activeBlob . Uri . AbsoluteUri } ") ;
536+ await currentBlob . activeBlob . DeleteIfExistsAsync ( ) ;
537+ Trace . TraceInformation ( $ "Deleted ReadMe of { edit . Id } { edit . Version } from { currentBlob . activeBlob . Uri . AbsoluteUri } ") ;
538+ }
539+ return currentBlob ;
540+ }
541+
542+ private async Task RollBackReadMeAsync ( PackageEdit edit , string directory , string originalReadMePath ,
543+ CloudBlockBlob activeReadMeSnapshot , CloudBlockBlob activeReadMeBlob )
544+ {
545+ if ( edit . ReadMeState != null )
546+ {
547+ if ( edit . ReadMeState == ReadMeChanged )
548+ {
549+ if ( edit . HasReadMe )
550+ {
551+ Trace . TraceWarning (
552+ $ "Rolling back ReadMe blob for { edit . Id } { edit . Version } . Copying snapshot { activeReadMeSnapshot . Uri . AbsoluteUri } to { activeReadMeBlob . Uri . AbsoluteUri } ") ;
553+ activeReadMeBlob . StartCopy ( activeReadMeSnapshot ) ;
554+ while ( activeReadMeBlob . CopyState . Status != CopyStatus . Success )
555+ {
556+ await Task . Delay ( 1000 ) ;
557+ }
558+ Trace . TraceWarning (
559+ $ "Rolled back ReadMe blob for { edit . Id } { edit . Version } . Copying snapshot { activeReadMeSnapshot . Uri . AbsoluteUri } to { activeReadMeBlob . Uri . AbsoluteUri } ") ;
560+ }
561+ else
562+ {
563+ // Delete ReadMes from active
564+ Trace . TraceInformation ( $ "Deleting ReadMe of { edit . Id } { edit . Version } from { activeReadMeBlob . Uri . AbsoluteUri } ") ;
565+ await activeReadMeBlob . DeleteIfExistsAsync ( ) ;
566+ Trace . TraceInformation ( $ "Deleted ReadMe of { edit . Id } { edit . Version } from { activeReadMeBlob . Uri . AbsoluteUri } ") ;
567+ }
568+ }
569+ else if ( edit . ReadMeState == ReadMeDeleted )
570+ {
571+ try
572+ {
573+ // Upload original ReadMe back to active
574+ Trace . TraceInformation ( $ "Uploading old ReadMe for { edit . Id } { edit . Version } to { activeReadMeBlob . Uri . AbsoluteUri } ") ;
575+ await activeReadMeBlob . UploadFromFileAsync ( originalReadMePath ) ;
576+ Trace . TraceInformation ( $ "Uploaded old ReadMe for { edit . Id } { edit . Version } to { activeReadMeBlob . Uri . AbsoluteUri } ") ;
577+ }
578+ finally
579+ {
580+ if ( ! string . IsNullOrEmpty ( originalReadMePath ) && File . Exists ( originalReadMePath ) )
581+ {
582+ File . Delete ( originalReadMePath ) ;
583+ }
584+ }
585+ }
586+ }
393587 }
394588 }
395589}
0 commit comments