|
1 | 1 | package coderd
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "archive/tar" |
| 5 | + "bytes" |
4 | 6 | "context"
|
5 | 7 | "database/sql"
|
6 | 8 | "errors"
|
7 | 9 | "fmt"
|
| 10 | + "io" |
8 | 11 | "net/http"
|
9 | 12 | "sort"
|
10 | 13 | "time"
|
@@ -32,6 +35,7 @@ import (
|
32 | 35 | "github.com/coder/coder/v2/coderd/workspacestats"
|
33 | 36 | "github.com/coder/coder/v2/codersdk"
|
34 | 37 | "github.com/coder/coder/v2/examples"
|
| 38 | + "compress/gzip" |
35 | 39 | )
|
36 | 40 |
|
37 | 41 | // Returns a single template.
|
@@ -1100,3 +1104,109 @@ func findTemplateAdmins(ctx context.Context, store database.Store) ([]database.G
|
1100 | 1104 | }
|
1101 | 1105 | return append(owners, templateAdmins...), nil
|
1102 | 1106 | }
|
| 1107 | + |
| 1108 | +// @Summary Export template by ID |
| 1109 | +// @ID export-template-by-id |
| 1110 | +// @Security CoderSessionToken |
| 1111 | +// @Produce application/x-gzip |
| 1112 | +// @Tags Templates |
| 1113 | +// @Param template path string true "Template ID" format(uuid) |
| 1114 | +// @Success 200 {file} binary "Template archive" |
| 1115 | +// @Router /templates/{template}/export [get] |
| 1116 | +func (api *API) exportTemplate(rw http.ResponseWriter, r *http.Request) { |
| 1117 | + ctx := r.Context() |
| 1118 | + template := httpmw.TemplateParam(r) |
| 1119 | + |
| 1120 | + // Get the latest version of the template |
| 1121 | + version, err := api.Database.GetTemplateVersionByTemplateIDAndName(ctx, database.GetTemplateVersionByTemplateIDAndNameParams{ |
| 1122 | + TemplateID: template.ID, |
| 1123 | + Name: template.ActiveVersionID.String(), |
| 1124 | + }) |
| 1125 | + if err != nil { |
| 1126 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 1127 | + Message: "Failed to get template version.", |
| 1128 | + Detail: err.Error(), |
| 1129 | + }) |
| 1130 | + return |
| 1131 | + } |
| 1132 | + |
| 1133 | + // Create a buffer to store our archive |
| 1134 | + var buf bytes.Buffer |
| 1135 | + |
| 1136 | + // Create gzip writer |
| 1137 | + gw := gzip.NewWriter(&buf) |
| 1138 | + tw := tar.NewWriter(gw) |
| 1139 | + |
| 1140 | + // Add template files to archive |
| 1141 | + files := []struct { |
| 1142 | + Name string |
| 1143 | + Content string |
| 1144 | + }{ |
| 1145 | + { |
| 1146 | + Name: "main.tf", |
| 1147 | + Content: version.Provisioner, |
| 1148 | + }, |
| 1149 | + { |
| 1150 | + Name: "README.md", |
| 1151 | + Content: template.Description, |
| 1152 | + }, |
| 1153 | + } |
| 1154 | + |
| 1155 | + for _, file := range files { |
| 1156 | + hdr := &tar.Header{ |
| 1157 | + Name: file.Name, |
| 1158 | + Mode: 0644, |
| 1159 | + Size: int64(len(file.Content)), |
| 1160 | + ModTime: time.Now(), |
| 1161 | + Format: tar.FormatPAX, |
| 1162 | + } |
| 1163 | + |
| 1164 | + if err := tw.WriteHeader(hdr); err != nil { |
| 1165 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 1166 | + Message: "Failed to write tar header.", |
| 1167 | + Detail: err.Error(), |
| 1168 | + }) |
| 1169 | + return |
| 1170 | + } |
| 1171 | + |
| 1172 | + if _, err := tw.Write([]byte(file.Content)); err != nil { |
| 1173 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 1174 | + Message: "Failed to write file content.", |
| 1175 | + Detail: err.Error(), |
| 1176 | + }) |
| 1177 | + return |
| 1178 | + } |
| 1179 | + } |
| 1180 | + |
| 1181 | + // Close tar writer |
| 1182 | + if err := tw.Close(); err != nil { |
| 1183 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 1184 | + Message: "Failed to close tar writer.", |
| 1185 | + Detail: err.Error(), |
| 1186 | + }) |
| 1187 | + return |
| 1188 | + } |
| 1189 | + |
| 1190 | + // Close gzip writer |
| 1191 | + if err := gw.Close(); err != nil { |
| 1192 | + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ |
| 1193 | + Message: "Failed to close gzip writer.", |
| 1194 | + Detail: err.Error(), |
| 1195 | + }) |
| 1196 | + return |
| 1197 | + } |
| 1198 | + |
| 1199 | + // Set response headers |
| 1200 | + rw.Header().Set("Content-Type", "application/x-gzip") |
| 1201 | + rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz", template.Name)) |
| 1202 | + rw.Header().Set("Content-Length", fmt.Sprintf("%d", buf.Len())) |
| 1203 | + |
| 1204 | + // Write the archive to the response |
| 1205 | + if _, err := io.Copy(rw, &buf); err != nil { |
| 1206 | + api.Logger.Error(ctx, "failed to write template archive to response", |
| 1207 | + slog.Error(err), |
| 1208 | + slog.F("template_id", template.ID), |
| 1209 | + ) |
| 1210 | + return |
| 1211 | + } |
| 1212 | +} |
0 commit comments