code.gitea.io/gitea@v1.22.3/services/packages/rpm/repository.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package rpm
     5  
     6  import (
     7  	"bytes"
     8  	"compress/gzip"
     9  	"context"
    10  	"crypto/sha256"
    11  	"encoding/hex"
    12  	"encoding/xml"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"strings"
    17  	"time"
    18  
    19  	packages_model "code.gitea.io/gitea/models/packages"
    20  	rpm_model "code.gitea.io/gitea/models/packages/rpm"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	"code.gitea.io/gitea/modules/json"
    23  	packages_module "code.gitea.io/gitea/modules/packages"
    24  	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
    25  	"code.gitea.io/gitea/modules/util"
    26  	packages_service "code.gitea.io/gitea/services/packages"
    27  
    28  	"github.com/keybase/go-crypto/openpgp"
    29  	"github.com/keybase/go-crypto/openpgp/armor"
    30  	"github.com/keybase/go-crypto/openpgp/packet"
    31  )
    32  
    33  // GetOrCreateRepositoryVersion gets or creates the internal repository package
    34  // The RPM registry needs multiple metadata files which are stored in this package.
    35  func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
    36  	return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeRpm, rpm_module.RepositoryPackage, rpm_module.RepositoryVersion)
    37  }
    38  
    39  // GetOrCreateKeyPair gets or creates the PGP keys used to sign repository metadata files
    40  func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
    41  	priv, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPrivate)
    42  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    43  		return "", "", err
    44  	}
    45  
    46  	pub, err := user_model.GetSetting(ctx, ownerID, rpm_module.SettingKeyPublic)
    47  	if err != nil && !errors.Is(err, util.ErrNotExist) {
    48  		return "", "", err
    49  	}
    50  
    51  	if priv == "" || pub == "" {
    52  		priv, pub, err = generateKeypair()
    53  		if err != nil {
    54  			return "", "", err
    55  		}
    56  
    57  		if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPrivate, priv); err != nil {
    58  			return "", "", err
    59  		}
    60  
    61  		if err := user_model.SetUserSetting(ctx, ownerID, rpm_module.SettingKeyPublic, pub); err != nil {
    62  			return "", "", err
    63  		}
    64  	}
    65  
    66  	return priv, pub, nil
    67  }
    68  
    69  func generateKeypair() (string, string, error) {
    70  	e, err := openpgp.NewEntity("", "RPM Registry", "", nil)
    71  	if err != nil {
    72  		return "", "", err
    73  	}
    74  
    75  	var priv strings.Builder
    76  	var pub strings.Builder
    77  
    78  	w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
    79  	if err != nil {
    80  		return "", "", err
    81  	}
    82  	if err := e.SerializePrivate(w, nil); err != nil {
    83  		return "", "", err
    84  	}
    85  	w.Close()
    86  
    87  	w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
    88  	if err != nil {
    89  		return "", "", err
    90  	}
    91  	if err := e.Serialize(w); err != nil {
    92  		return "", "", err
    93  	}
    94  	w.Close()
    95  
    96  	return priv.String(), pub.String(), nil
    97  }
    98  
    99  // BuildAllRepositoryFiles (re)builds all repository files for every available group
   100  func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
   101  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	// 1. Delete all existing repository files
   107  	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
   108  	if err != nil {
   109  		return err
   110  	}
   111  
   112  	for _, pf := range pfs {
   113  		if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   114  			return err
   115  		}
   116  	}
   117  
   118  	// 2. (Re)Build repository files for existing packages
   119  	groups, err := rpm_model.GetGroups(ctx, ownerID)
   120  	if err != nil {
   121  		return err
   122  	}
   123  	for _, group := range groups {
   124  		if err := BuildSpecificRepositoryFiles(ctx, ownerID, group); err != nil {
   125  			return fmt.Errorf("failed to build repository files [%s]: %w", group, err)
   126  		}
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  type repoChecksum struct {
   133  	Value string `xml:",chardata"`
   134  	Type  string `xml:"type,attr"`
   135  }
   136  
   137  type repoLocation struct {
   138  	Href string `xml:"href,attr"`
   139  }
   140  
   141  type repoData struct {
   142  	Type         string       `xml:"type,attr"`
   143  	Checksum     repoChecksum `xml:"checksum"`
   144  	OpenChecksum repoChecksum `xml:"open-checksum"`
   145  	Location     repoLocation `xml:"location"`
   146  	Timestamp    int64        `xml:"timestamp"`
   147  	Size         int64        `xml:"size"`
   148  	OpenSize     int64        `xml:"open-size"`
   149  }
   150  
   151  type packageData struct {
   152  	Package         *packages_model.Package
   153  	Version         *packages_model.PackageVersion
   154  	Blob            *packages_model.PackageBlob
   155  	VersionMetadata *rpm_module.VersionMetadata
   156  	FileMetadata    *rpm_module.FileMetadata
   157  }
   158  
   159  type packageCache = map[*packages_model.PackageFile]*packageData
   160  
   161  // BuildSpecificRepositoryFiles builds metadata files for the repository
   162  func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, group string) error {
   163  	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
   169  		OwnerID:      ownerID,
   170  		PackageType:  packages_model.TypeRpm,
   171  		Query:        "%.rpm",
   172  		CompositeKey: group,
   173  	})
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// Delete the repository files if there are no packages
   179  	if len(pfs) == 0 {
   180  		pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		for _, pf := range pfs {
   185  			if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
   186  				return err
   187  			}
   188  		}
   189  
   190  		return nil
   191  	}
   192  
   193  	// Cache data needed for all repository files
   194  	cache := make(packageCache)
   195  	for _, pf := range pfs {
   196  		pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
   197  		if err != nil {
   198  			return err
   199  		}
   200  		p, err := packages_model.GetPackageByID(ctx, pv.PackageID)
   201  		if err != nil {
   202  			return err
   203  		}
   204  		pb, err := packages_model.GetBlobByID(ctx, pf.BlobID)
   205  		if err != nil {
   206  			return err
   207  		}
   208  		pps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, rpm_module.PropertyMetadata)
   209  		if err != nil {
   210  			return err
   211  		}
   212  
   213  		pd := &packageData{
   214  			Package: p,
   215  			Version: pv,
   216  			Blob:    pb,
   217  		}
   218  
   219  		if err := json.Unmarshal([]byte(pv.MetadataJSON), &pd.VersionMetadata); err != nil {
   220  			return err
   221  		}
   222  		if len(pps) > 0 {
   223  			if err := json.Unmarshal([]byte(pps[0].Value), &pd.FileMetadata); err != nil {
   224  				return err
   225  			}
   226  		}
   227  
   228  		cache[pf] = pd
   229  	}
   230  
   231  	primary, err := buildPrimary(ctx, pv, pfs, cache, group)
   232  	if err != nil {
   233  		return err
   234  	}
   235  	filelists, err := buildFilelists(ctx, pv, pfs, cache, group)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	other, err := buildOther(ctx, pv, pfs, cache, group)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	return buildRepomd(
   245  		ctx,
   246  		pv,
   247  		ownerID,
   248  		[]*repoData{
   249  			primary,
   250  			filelists,
   251  			other,
   252  		},
   253  		group,
   254  	)
   255  }
   256  
   257  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#repomd-xml
   258  func buildRepomd(ctx context.Context, pv *packages_model.PackageVersion, ownerID int64, data []*repoData, group string) error {
   259  	type Repomd struct {
   260  		XMLName  xml.Name    `xml:"repomd"`
   261  		Xmlns    string      `xml:"xmlns,attr"`
   262  		XmlnsRpm string      `xml:"xmlns:rpm,attr"`
   263  		Data     []*repoData `xml:"data"`
   264  	}
   265  
   266  	var buf bytes.Buffer
   267  	buf.WriteString(xml.Header)
   268  	if err := xml.NewEncoder(&buf).Encode(&Repomd{
   269  		Xmlns:    "http://linux.duke.edu/metadata/repo",
   270  		XmlnsRpm: "http://linux.duke.edu/metadata/rpm",
   271  		Data:     data,
   272  	}); err != nil {
   273  		return err
   274  	}
   275  
   276  	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	block, err := armor.Decode(strings.NewReader(priv))
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
   287  	if err != nil {
   288  		return err
   289  	}
   290  
   291  	repomdAscContent, _ := packages_module.NewHashedBuffer()
   292  	defer repomdAscContent.Close()
   293  
   294  	if err := openpgp.ArmoredDetachSign(repomdAscContent, e, bytes.NewReader(buf.Bytes()), nil); err != nil {
   295  		return err
   296  	}
   297  
   298  	repomdContent, _ := packages_module.CreateHashedBufferFromReader(&buf)
   299  	defer repomdContent.Close()
   300  
   301  	for _, file := range []struct {
   302  		Name string
   303  		Data packages_module.HashedSizeReader
   304  	}{
   305  		{"repomd.xml", repomdContent},
   306  		{"repomd.xml.asc", repomdAscContent},
   307  	} {
   308  		_, err = packages_service.AddFileToPackageVersionInternal(
   309  			ctx,
   310  			pv,
   311  			&packages_service.PackageFileCreationInfo{
   312  				PackageFileInfo: packages_service.PackageFileInfo{
   313  					Filename:     file.Name,
   314  					CompositeKey: group,
   315  				},
   316  				Creator:           user_model.NewGhostUser(),
   317  				Data:              file.Data,
   318  				IsLead:            false,
   319  				OverwriteExisting: true,
   320  			},
   321  		)
   322  		if err != nil {
   323  			return err
   324  		}
   325  	}
   326  
   327  	return nil
   328  }
   329  
   330  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#primary-xml
   331  func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) {
   332  	type Version struct {
   333  		Epoch   string `xml:"epoch,attr"`
   334  		Version string `xml:"ver,attr"`
   335  		Release string `xml:"rel,attr"`
   336  	}
   337  
   338  	type Checksum struct {
   339  		Checksum string `xml:",chardata"`
   340  		Type     string `xml:"type,attr"`
   341  		Pkgid    string `xml:"pkgid,attr"`
   342  	}
   343  
   344  	type Times struct {
   345  		File  uint64 `xml:"file,attr"`
   346  		Build uint64 `xml:"build,attr"`
   347  	}
   348  
   349  	type Sizes struct {
   350  		Package   int64  `xml:"package,attr"`
   351  		Installed uint64 `xml:"installed,attr"`
   352  		Archive   uint64 `xml:"archive,attr"`
   353  	}
   354  
   355  	type Location struct {
   356  		Href string `xml:"href,attr"`
   357  	}
   358  
   359  	type EntryList struct {
   360  		Entries []*rpm_module.Entry `xml:"rpm:entry"`
   361  	}
   362  
   363  	type Format struct {
   364  		License   string             `xml:"rpm:license"`
   365  		Vendor    string             `xml:"rpm:vendor"`
   366  		Group     string             `xml:"rpm:group"`
   367  		Buildhost string             `xml:"rpm:buildhost"`
   368  		Sourcerpm string             `xml:"rpm:sourcerpm"`
   369  		Provides  EntryList          `xml:"rpm:provides"`
   370  		Requires  EntryList          `xml:"rpm:requires"`
   371  		Conflicts EntryList          `xml:"rpm:conflicts"`
   372  		Obsoletes EntryList          `xml:"rpm:obsoletes"`
   373  		Files     []*rpm_module.File `xml:"file"`
   374  	}
   375  
   376  	type Package struct {
   377  		XMLName      xml.Name `xml:"package"`
   378  		Type         string   `xml:"type,attr"`
   379  		Name         string   `xml:"name"`
   380  		Architecture string   `xml:"arch"`
   381  		Version      Version  `xml:"version"`
   382  		Checksum     Checksum `xml:"checksum"`
   383  		Summary      string   `xml:"summary"`
   384  		Description  string   `xml:"description"`
   385  		Packager     string   `xml:"packager"`
   386  		URL          string   `xml:"url"`
   387  		Time         Times    `xml:"time"`
   388  		Size         Sizes    `xml:"size"`
   389  		Location     Location `xml:"location"`
   390  		Format       Format   `xml:"format"`
   391  	}
   392  
   393  	type Metadata struct {
   394  		XMLName      xml.Name   `xml:"metadata"`
   395  		Xmlns        string     `xml:"xmlns,attr"`
   396  		XmlnsRpm     string     `xml:"xmlns:rpm,attr"`
   397  		PackageCount int        `xml:"packages,attr"`
   398  		Packages     []*Package `xml:"package"`
   399  	}
   400  
   401  	packages := make([]*Package, 0, len(pfs))
   402  	for _, pf := range pfs {
   403  		pd := c[pf]
   404  
   405  		files := make([]*rpm_module.File, 0, 3)
   406  		for _, f := range pd.FileMetadata.Files {
   407  			if f.IsExecutable {
   408  				files = append(files, f)
   409  			}
   410  		}
   411  		packageVersion := fmt.Sprintf("%s-%s", pd.FileMetadata.Version, pd.FileMetadata.Release)
   412  		packages = append(packages, &Package{
   413  			Type:         "rpm",
   414  			Name:         pd.Package.Name,
   415  			Architecture: pd.FileMetadata.Architecture,
   416  			Version: Version{
   417  				Epoch:   pd.FileMetadata.Epoch,
   418  				Version: pd.FileMetadata.Version,
   419  				Release: pd.FileMetadata.Release,
   420  			},
   421  			Checksum: Checksum{
   422  				Type:     "sha256",
   423  				Checksum: pd.Blob.HashSHA256,
   424  				Pkgid:    "YES",
   425  			},
   426  			Summary:     pd.VersionMetadata.Summary,
   427  			Description: pd.VersionMetadata.Description,
   428  			Packager:    pd.FileMetadata.Packager,
   429  			URL:         pd.VersionMetadata.ProjectURL,
   430  			Time: Times{
   431  				File:  pd.FileMetadata.FileTime,
   432  				Build: pd.FileMetadata.BuildTime,
   433  			},
   434  			Size: Sizes{
   435  				Package:   pd.Blob.Size,
   436  				Installed: pd.FileMetadata.InstalledSize,
   437  				Archive:   pd.FileMetadata.ArchiveSize,
   438  			},
   439  			Location: Location{
   440  				Href: fmt.Sprintf("package/%s/%s/%s/%s-%s.%s.rpm", pd.Package.Name, packageVersion, pd.FileMetadata.Architecture, pd.Package.Name, packageVersion, pd.FileMetadata.Architecture),
   441  			},
   442  			Format: Format{
   443  				License:   pd.VersionMetadata.License,
   444  				Vendor:    pd.FileMetadata.Vendor,
   445  				Group:     pd.FileMetadata.Group,
   446  				Buildhost: pd.FileMetadata.BuildHost,
   447  				Sourcerpm: pd.FileMetadata.SourceRpm,
   448  				Provides: EntryList{
   449  					Entries: pd.FileMetadata.Provides,
   450  				},
   451  				Requires: EntryList{
   452  					Entries: pd.FileMetadata.Requires,
   453  				},
   454  				Conflicts: EntryList{
   455  					Entries: pd.FileMetadata.Conflicts,
   456  				},
   457  				Obsoletes: EntryList{
   458  					Entries: pd.FileMetadata.Obsoletes,
   459  				},
   460  				Files: files,
   461  			},
   462  		})
   463  	}
   464  
   465  	return addDataAsFileToRepo(ctx, pv, "primary", &Metadata{
   466  		Xmlns:        "http://linux.duke.edu/metadata/common",
   467  		XmlnsRpm:     "http://linux.duke.edu/metadata/rpm",
   468  		PackageCount: len(pfs),
   469  		Packages:     packages,
   470  	}, group)
   471  }
   472  
   473  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml
   474  func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl
   475  	type Version struct {
   476  		Epoch   string `xml:"epoch,attr"`
   477  		Version string `xml:"ver,attr"`
   478  		Release string `xml:"rel,attr"`
   479  	}
   480  
   481  	type Package struct {
   482  		Pkgid        string             `xml:"pkgid,attr"`
   483  		Name         string             `xml:"name,attr"`
   484  		Architecture string             `xml:"arch,attr"`
   485  		Version      Version            `xml:"version"`
   486  		Files        []*rpm_module.File `xml:"file"`
   487  	}
   488  
   489  	type Filelists struct {
   490  		XMLName      xml.Name   `xml:"filelists"`
   491  		Xmlns        string     `xml:"xmlns,attr"`
   492  		PackageCount int        `xml:"packages,attr"`
   493  		Packages     []*Package `xml:"package"`
   494  	}
   495  
   496  	packages := make([]*Package, 0, len(pfs))
   497  	for _, pf := range pfs {
   498  		pd := c[pf]
   499  
   500  		packages = append(packages, &Package{
   501  			Pkgid:        pd.Blob.HashSHA256,
   502  			Name:         pd.Package.Name,
   503  			Architecture: pd.FileMetadata.Architecture,
   504  			Version: Version{
   505  				Epoch:   pd.FileMetadata.Epoch,
   506  				Version: pd.FileMetadata.Version,
   507  				Release: pd.FileMetadata.Release,
   508  			},
   509  			Files: pd.FileMetadata.Files,
   510  		})
   511  	}
   512  
   513  	return addDataAsFileToRepo(ctx, pv, "filelists", &Filelists{
   514  		Xmlns:        "http://linux.duke.edu/metadata/other",
   515  		PackageCount: len(pfs),
   516  		Packages:     packages,
   517  	}, group)
   518  }
   519  
   520  // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml
   521  func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl
   522  	type Version struct {
   523  		Epoch   string `xml:"epoch,attr"`
   524  		Version string `xml:"ver,attr"`
   525  		Release string `xml:"rel,attr"`
   526  	}
   527  
   528  	type Package struct {
   529  		Pkgid        string                  `xml:"pkgid,attr"`
   530  		Name         string                  `xml:"name,attr"`
   531  		Architecture string                  `xml:"arch,attr"`
   532  		Version      Version                 `xml:"version"`
   533  		Changelogs   []*rpm_module.Changelog `xml:"changelog"`
   534  	}
   535  
   536  	type Otherdata struct {
   537  		XMLName      xml.Name   `xml:"otherdata"`
   538  		Xmlns        string     `xml:"xmlns,attr"`
   539  		PackageCount int        `xml:"packages,attr"`
   540  		Packages     []*Package `xml:"package"`
   541  	}
   542  
   543  	packages := make([]*Package, 0, len(pfs))
   544  	for _, pf := range pfs {
   545  		pd := c[pf]
   546  
   547  		packages = append(packages, &Package{
   548  			Pkgid:        pd.Blob.HashSHA256,
   549  			Name:         pd.Package.Name,
   550  			Architecture: pd.FileMetadata.Architecture,
   551  			Version: Version{
   552  				Epoch:   pd.FileMetadata.Epoch,
   553  				Version: pd.FileMetadata.Version,
   554  				Release: pd.FileMetadata.Release,
   555  			},
   556  			Changelogs: pd.FileMetadata.Changelogs,
   557  		})
   558  	}
   559  
   560  	return addDataAsFileToRepo(ctx, pv, "other", &Otherdata{
   561  		Xmlns:        "http://linux.duke.edu/metadata/other",
   562  		PackageCount: len(pfs),
   563  		Packages:     packages,
   564  	}, group)
   565  }
   566  
   567  // writtenCounter counts all written bytes
   568  type writtenCounter struct {
   569  	written int64
   570  }
   571  
   572  func (wc *writtenCounter) Write(buf []byte) (int, error) {
   573  	n := len(buf)
   574  
   575  	wc.written += int64(n)
   576  
   577  	return n, nil
   578  }
   579  
   580  func (wc *writtenCounter) Written() int64 {
   581  	return wc.written
   582  }
   583  
   584  func addDataAsFileToRepo(ctx context.Context, pv *packages_model.PackageVersion, filetype string, obj any, group string) (*repoData, error) {
   585  	content, _ := packages_module.NewHashedBuffer()
   586  	defer content.Close()
   587  
   588  	gzw := gzip.NewWriter(content)
   589  	wc := &writtenCounter{}
   590  	h := sha256.New()
   591  
   592  	w := io.MultiWriter(gzw, wc, h)
   593  	_, _ = w.Write([]byte(xml.Header))
   594  
   595  	if err := xml.NewEncoder(w).Encode(obj); err != nil {
   596  		return nil, err
   597  	}
   598  
   599  	if err := gzw.Close(); err != nil {
   600  		return nil, err
   601  	}
   602  
   603  	filename := filetype + ".xml.gz"
   604  
   605  	_, err := packages_service.AddFileToPackageVersionInternal(
   606  		ctx,
   607  		pv,
   608  		&packages_service.PackageFileCreationInfo{
   609  			PackageFileInfo: packages_service.PackageFileInfo{
   610  				Filename:     filename,
   611  				CompositeKey: group,
   612  			},
   613  			Creator:           user_model.NewGhostUser(),
   614  			Data:              content,
   615  			IsLead:            false,
   616  			OverwriteExisting: true,
   617  		},
   618  	)
   619  	if err != nil {
   620  		return nil, err
   621  	}
   622  
   623  	_, _, hashSHA256, _ := content.Sums()
   624  
   625  	return &repoData{
   626  		Type: filetype,
   627  		Checksum: repoChecksum{
   628  			Type:  "sha256",
   629  			Value: hex.EncodeToString(hashSHA256),
   630  		},
   631  		OpenChecksum: repoChecksum{
   632  			Type:  "sha256",
   633  			Value: hex.EncodeToString(h.Sum(nil)),
   634  		},
   635  		Location: repoLocation{
   636  			Href: "repodata/" + filename,
   637  		},
   638  		Timestamp: time.Now().Unix(),
   639  		Size:      content.Size(),
   640  		OpenSize:  wc.Written(),
   641  	}, nil
   642  }