@@ -217,6 +217,16 @@ func (s *LocalDirV1) handleV1Query(w http.ResponseWriter, r *http.Request) {
217217 }
218218 defer catalogFile .Close ()
219219
220+ w .Header ().Set ("Last-Modified" , catalogStat .ModTime ().UTC ().Format (TimeFormat ))
221+ switch checkIfModifiedSince (r , w , catalogStat .ModTime ()) {
222+ case condNone :
223+ return
224+ case condFalse :
225+ w .WriteHeader (http .StatusNotModified )
226+ return
227+ case condTrue :
228+ }
229+
220230 schema := r .URL .Query ().Get ("schema" )
221231 pkg := r .URL .Query ().Get ("package" )
222232 name := r .URL .Query ().Get ("name" )
@@ -298,3 +308,65 @@ func (s *LocalDirV1) getIndex(catalog string) (*index, error) {
298308 }
299309 return idx .(* index ), nil
300310}
311+
312+ type condResult int
313+
314+ const (
315+ condNone condResult = iota
316+ condTrue
317+ condFalse
318+ )
319+
320+ func checkIfModifiedSince (r * http.Request , w http.ResponseWriter , modtime time.Time ) condResult {
321+ ims := r .Header .Get ("If-Modified-Since" )
322+ if ims == "" || isZeroTime (modtime ) {
323+ return condTrue
324+ }
325+ t , err := ParseTime (ims )
326+ if err != nil {
327+ httpError (w , err )
328+ return condNone
329+ }
330+ // The Last-Modified header truncates sub-second precision so
331+ // the modtime needs to be truncated too.
332+ modtime = modtime .Truncate (time .Second )
333+ if modtime .Compare (t ) <= 0 {
334+ return condFalse
335+ }
336+ return condTrue
337+ }
338+
339+ // TimeFormat is the time format to use when generating times in HTTP
340+ // headers. It is like [time.RFC1123] but hard-codes GMT as the time
341+ // zone. The time being formatted must be in UTC for Format to
342+ // generate the correct format.
343+ //
344+ // For parsing this time format, see [ParseTime].
345+ const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
346+
347+ var (
348+ unixEpochTime = time .Unix (0 , 0 )
349+ timeFormats = []string {
350+ TimeFormat ,
351+ time .RFC850 ,
352+ time .ANSIC ,
353+ }
354+ )
355+
356+ // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
357+ func isZeroTime (t time.Time ) bool {
358+ return t .IsZero () || t .Equal (unixEpochTime )
359+ }
360+
361+ // ParseTime parses a time header (such as the Date: header),
362+ // trying each of the three formats allowed by HTTP/1.1:
363+ // [TimeFormat], [time.RFC850], and [time.ANSIC].
364+ func ParseTime (text string ) (t time.Time , err error ) {
365+ for _ , layout := range timeFormats {
366+ t , err = time .Parse (layout , text )
367+ if err == nil {
368+ return
369+ }
370+ }
371+ return
372+ }
0 commit comments