github.com/swiftstack/proxyfs@v0.0.0-20201223034610-5434d919416e/httpserver/request_handler.go (about)

     1  package httpserver
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"math"
     9  	"net/http"
    10  	"net/http/pprof"
    11  	"net/url"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/swiftstack/sortedmap"
    18  
    19  	"github.com/swiftstack/ProxyFS/bucketstats"
    20  	"github.com/swiftstack/ProxyFS/fs"
    21  	"github.com/swiftstack/ProxyFS/halter"
    22  	"github.com/swiftstack/ProxyFS/headhunter"
    23  	"github.com/swiftstack/ProxyFS/inode"
    24  	"github.com/swiftstack/ProxyFS/jrpcfs"
    25  	"github.com/swiftstack/ProxyFS/liveness"
    26  	"github.com/swiftstack/ProxyFS/logger"
    27  	"github.com/swiftstack/ProxyFS/stats"
    28  	"github.com/swiftstack/ProxyFS/utils"
    29  	"github.com/swiftstack/ProxyFS/version"
    30  )
    31  
    32  type httpRequestHandler struct{}
    33  
    34  type requestStateStruct struct {
    35  	pathSplit               []string
    36  	numPathParts            int
    37  	formatResponseAsJSON    bool
    38  	formatResponseCompactly bool
    39  	performValidation       bool
    40  	percentRange            string
    41  	startNonce              uint64
    42  	volume                  *volumeStruct
    43  }
    44  
    45  func serveHTTP() {
    46  	_ = http.Serve(globals.netListener, httpRequestHandler{})
    47  	globals.wg.Done()
    48  }
    49  
    50  func (h httpRequestHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) {
    51  	globals.Lock()
    52  	if globals.active {
    53  		switch request.Method {
    54  		case http.MethodDelete:
    55  			doDelete(responseWriter, request)
    56  		case http.MethodGet:
    57  			doGet(responseWriter, request)
    58  		case http.MethodPost:
    59  			doPost(responseWriter, request)
    60  		case http.MethodPut:
    61  			doPut(responseWriter, request)
    62  		default:
    63  			responseWriter.WriteHeader(http.StatusMethodNotAllowed)
    64  		}
    65  	} else {
    66  		responseWriter.WriteHeader(http.StatusServiceUnavailable)
    67  	}
    68  	globals.Unlock()
    69  }
    70  
    71  func doDelete(responseWriter http.ResponseWriter, request *http.Request) {
    72  	switch {
    73  	case strings.HasPrefix(request.URL.Path, "/volume"):
    74  		doDeleteOfVolume(responseWriter, request)
    75  	default:
    76  		responseWriter.WriteHeader(http.StatusNotFound)
    77  	}
    78  }
    79  
    80  func doDeleteOfVolume(responseWriter http.ResponseWriter, request *http.Request) {
    81  	var (
    82  		err           error
    83  		numPathParts  int
    84  		ok            bool
    85  		pathSplit     []string
    86  		snapShotID    uint64
    87  		volume        *volumeStruct
    88  		volumeAsValue sortedmap.Value
    89  		volumeName    string
    90  	)
    91  
    92  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
    93  	//                                                  pathSplit[1] should be "volume" based on how we got here
    94  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
    95  	numPathParts = len(pathSplit) - 1
    96  	if "" == pathSplit[numPathParts] {
    97  		numPathParts--
    98  	}
    99  
   100  	if "volume" != pathSplit[1] {
   101  		responseWriter.WriteHeader(http.StatusNotFound)
   102  		return
   103  	}
   104  
   105  	if 4 != numPathParts {
   106  		responseWriter.WriteHeader(http.StatusNotFound)
   107  		return
   108  	}
   109  
   110  	volumeName = pathSplit[2]
   111  
   112  	volumeAsValue, ok, err = globals.volumeLLRB.GetByKey(volumeName)
   113  	if nil != err {
   114  		logger.Fatalf("HTTP Server Logic Error: %v", err)
   115  	}
   116  	if !ok {
   117  		responseWriter.WriteHeader(http.StatusNotFound)
   118  		return
   119  	}
   120  	volume = volumeAsValue.(*volumeStruct)
   121  
   122  	if "snapshot" != pathSplit[3] {
   123  		responseWriter.WriteHeader(http.StatusNotFound)
   124  		return
   125  	}
   126  
   127  	// Form: /volume/<volume-name>/snapshot/<snapshot-id>
   128  
   129  	snapShotID, err = strconv.ParseUint(pathSplit[4], 10, 64)
   130  	if nil != err {
   131  		responseWriter.WriteHeader(http.StatusNotFound)
   132  		return
   133  	}
   134  
   135  	err = volume.inodeVolumeHandle.SnapShotDelete(snapShotID)
   136  	if nil == err {
   137  		responseWriter.WriteHeader(http.StatusNoContent)
   138  	} else {
   139  		responseWriter.WriteHeader(http.StatusNotFound)
   140  	}
   141  }
   142  
   143  func doGet(responseWriter http.ResponseWriter, request *http.Request) {
   144  	path := strings.TrimRight(request.URL.Path, "/")
   145  
   146  	switch {
   147  	case "" == path:
   148  		doGetOfIndexDotHTML(responseWriter, request)
   149  	case "/bootstrap.min.css" == path:
   150  		responseWriter.Header().Set("Content-Type", bootstrapDotCSSContentType)
   151  		responseWriter.WriteHeader(http.StatusOK)
   152  		_, _ = responseWriter.Write([]byte(bootstrapDotCSSContent))
   153  	case "/bootstrap.min.js" == path:
   154  		responseWriter.Header().Set("Content-Type", bootstrapDotJSContentType)
   155  		responseWriter.WriteHeader(http.StatusOK)
   156  		_, _ = responseWriter.Write([]byte(bootstrapDotJSContent))
   157  	case "/config" == path:
   158  		doGetOfConfig(responseWriter, request)
   159  	case "/index.html" == path:
   160  		doGetOfIndexDotHTML(responseWriter, request)
   161  	case "/jquery.min.js" == path:
   162  		responseWriter.Header().Set("Content-Type", jqueryDotJSContentType)
   163  		responseWriter.WriteHeader(http.StatusOK)
   164  		_, _ = responseWriter.Write([]byte(jqueryDotJSContent))
   165  	case "/jsontree.js" == path:
   166  		responseWriter.Header().Set("Content-Type", jsontreeDotJSContentType)
   167  		responseWriter.WriteHeader(http.StatusOK)
   168  		_, _ = responseWriter.Write([]byte(jsontreeDotJSContent))
   169  	case "/liveness" == path:
   170  		doGetOfLiveness(responseWriter, request)
   171  	case "/metrics" == path:
   172  		doGetOfMetrics(responseWriter, request)
   173  	case "/stats" == path:
   174  		doGetOfStats(responseWriter, request)
   175  	case "/debug/pprof/cmdline" == path:
   176  		pprof.Cmdline(responseWriter, request)
   177  	case "/debug/pprof/profile" == path:
   178  		pprof.Profile(responseWriter, request)
   179  	case "/debug/pprof/symbol" == path:
   180  		pprof.Symbol(responseWriter, request)
   181  	case "/debug/pprof/trace" == path:
   182  		pprof.Trace(responseWriter, request)
   183  	case strings.HasPrefix(request.URL.Path, "/debug/pprof"):
   184  		pprof.Index(responseWriter, request)
   185  	case "/open-iconic/font/css/open-iconic-bootstrap.min.css" == path:
   186  		responseWriter.Header().Set("Content-Type", openIconicBootstrapDotCSSContentType)
   187  		responseWriter.WriteHeader(http.StatusOK)
   188  		_, _ = responseWriter.Write([]byte(openIconicBootstrapDotCSSContent))
   189  	case "/open-iconic/font/fonts/open-iconic.eot" == path:
   190  		responseWriter.Header().Set("Content-Type", openIconicDotEOTContentType)
   191  		responseWriter.WriteHeader(http.StatusOK)
   192  		_, _ = responseWriter.Write(openIconicDotEOTContent)
   193  	case "/open-iconic/font/fonts/open-iconic.otf" == path:
   194  		responseWriter.Header().Set("Content-Type", openIconicDotOTFContentType)
   195  		responseWriter.WriteHeader(http.StatusOK)
   196  		_, _ = responseWriter.Write(openIconicDotOTFContent)
   197  	case "/open-iconic/font/fonts/open-iconic.svg" == path:
   198  		responseWriter.Header().Set("Content-Type", openIconicDotSVGContentType)
   199  		responseWriter.WriteHeader(http.StatusOK)
   200  		_, _ = responseWriter.Write([]byte(openIconicDotSVGContent))
   201  	case "/open-iconic/font/fonts/open-iconic.ttf" == path:
   202  		responseWriter.Header().Set("Content-Type", openIconicDotTTFContentType)
   203  		responseWriter.WriteHeader(http.StatusOK)
   204  		_, _ = responseWriter.Write(openIconicDotTTFContent)
   205  	case "/open-iconic/font/fonts/open-iconic.woff" == path:
   206  		responseWriter.Header().Set("Content-Type", openIconicDotWOFFContentType)
   207  		responseWriter.WriteHeader(http.StatusOK)
   208  		_, _ = responseWriter.Write(openIconicDotWOFFContent)
   209  	case "/popper.min.js" == path:
   210  		responseWriter.Header().Set("Content-Type", popperDotJSContentType)
   211  		responseWriter.WriteHeader(http.StatusOK)
   212  		_, _ = responseWriter.Write(popperDotJSContent)
   213  	case "/styles.css" == path:
   214  		responseWriter.Header().Set("Content-Type", stylesDotCSSContentType)
   215  		responseWriter.WriteHeader(http.StatusOK)
   216  		_, _ = responseWriter.Write([]byte(stylesDotCSSContent))
   217  	case "/version" == path:
   218  		responseWriter.Header().Set("Content-Type", "text/plain")
   219  		responseWriter.WriteHeader(http.StatusOK)
   220  		_, _ = responseWriter.Write([]byte(version.ProxyFSVersion))
   221  	case strings.HasPrefix(request.URL.Path, "/trigger"):
   222  		doGetOfTrigger(responseWriter, request)
   223  	case strings.HasPrefix(request.URL.Path, "/volume"):
   224  		doGetOfVolume(responseWriter, request)
   225  	default:
   226  		responseWriter.WriteHeader(http.StatusNotFound)
   227  	}
   228  }
   229  
   230  func doGetOfIndexDotHTML(responseWriter http.ResponseWriter, request *http.Request) {
   231  	responseWriter.Header().Set("Content-Type", "text/html")
   232  	responseWriter.WriteHeader(http.StatusOK)
   233  	_, _ = responseWriter.Write([]byte(fmt.Sprintf(indexDotHTMLTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort)))
   234  }
   235  
   236  func doGetOfConfig(responseWriter http.ResponseWriter, request *http.Request) {
   237  	var (
   238  		acceptHeader         string
   239  		confMapJSON          bytes.Buffer
   240  		confMapJSONPacked    []byte
   241  		formatResponseAsJSON bool
   242  		ok                   bool
   243  		paramList            []string
   244  		sendPackedConfig     bool
   245  	)
   246  
   247  	paramList, ok = request.URL.Query()["compact"]
   248  	if ok {
   249  		if 0 == len(paramList) {
   250  			sendPackedConfig = false
   251  		} else {
   252  			sendPackedConfig = !((paramList[0] == "") || (paramList[0] == "0") || (paramList[0] == "false"))
   253  		}
   254  	} else {
   255  		sendPackedConfig = false
   256  	}
   257  
   258  	acceptHeader = request.Header.Get("Accept")
   259  
   260  	if strings.Contains(acceptHeader, "application/json") {
   261  		formatResponseAsJSON = true
   262  	} else if strings.Contains(acceptHeader, "text/html") {
   263  		formatResponseAsJSON = false
   264  	} else if strings.Contains(acceptHeader, "*/*") {
   265  		formatResponseAsJSON = true
   266  	} else if strings.Contains(acceptHeader, "") {
   267  		formatResponseAsJSON = true
   268  	} else {
   269  		responseWriter.WriteHeader(http.StatusNotAcceptable)
   270  		return
   271  	}
   272  
   273  	confMapJSONPacked, _ = json.Marshal(globals.confMap)
   274  
   275  	if formatResponseAsJSON {
   276  		responseWriter.Header().Set("Content-Type", "application/json")
   277  		responseWriter.WriteHeader(http.StatusOK)
   278  
   279  		if sendPackedConfig {
   280  			_, _ = responseWriter.Write(confMapJSONPacked)
   281  		} else {
   282  			json.Indent(&confMapJSON, confMapJSONPacked, "", "\t")
   283  			_, _ = responseWriter.Write(confMapJSON.Bytes())
   284  			_, _ = responseWriter.Write([]byte("\n"))
   285  		}
   286  	} else {
   287  		responseWriter.Header().Set("Content-Type", "text/html")
   288  		responseWriter.WriteHeader(http.StatusOK)
   289  
   290  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(configTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, utils.ByteSliceToString(confMapJSONPacked))))
   291  	}
   292  }
   293  
   294  func doGetOfLiveness(responseWriter http.ResponseWriter, request *http.Request) {
   295  	var (
   296  		livenessReportAsJSON       bytes.Buffer
   297  		livenessReportAsJSONPacked []byte
   298  		livenessReportAsStruct     *liveness.LivenessReportStruct
   299  		ok                         bool
   300  		paramList                  []string
   301  		sendPackedReport           bool
   302  	)
   303  
   304  	// TODO: For now, assume JSON reponse requested
   305  
   306  	livenessReportAsStruct = liveness.FetchLivenessReport()
   307  
   308  	if nil == livenessReportAsStruct {
   309  		responseWriter.WriteHeader(http.StatusServiceUnavailable)
   310  		return
   311  	}
   312  
   313  	livenessReportAsJSONPacked, _ = json.Marshal(livenessReportAsStruct)
   314  
   315  	responseWriter.Header().Set("Content-Type", "application/json")
   316  	responseWriter.WriteHeader(http.StatusOK)
   317  
   318  	paramList, ok = request.URL.Query()["compact"]
   319  	if ok {
   320  		if 0 == len(paramList) {
   321  			sendPackedReport = false
   322  		} else {
   323  			sendPackedReport = !((paramList[0] == "") || (paramList[0] == "0") || (paramList[0] == "false"))
   324  		}
   325  	} else {
   326  		sendPackedReport = false
   327  	}
   328  
   329  	if sendPackedReport {
   330  		_, _ = responseWriter.Write(livenessReportAsJSONPacked)
   331  	} else {
   332  		json.Indent(&livenessReportAsJSON, livenessReportAsJSONPacked, "", "\t")
   333  		_, _ = responseWriter.Write(livenessReportAsJSON.Bytes())
   334  		_, _ = responseWriter.Write([]byte("\n"))
   335  	}
   336  }
   337  
   338  func doGetOfMetrics(responseWriter http.ResponseWriter, request *http.Request) {
   339  	var (
   340  		acceptHeader         string
   341  		err                  error
   342  		formatResponseAsHTML bool
   343  		formatResponseAsJSON bool
   344  		i                    int
   345  		memStats             runtime.MemStats
   346  		metricKey            string
   347  		metricValueAsString  string
   348  		metricValueAsUint64  uint64
   349  		metricsJSON          bytes.Buffer
   350  		metricsJSONPacked    []byte
   351  		metricsLLRB          sortedmap.LLRBTree
   352  		metricsMap           map[string]uint64
   353  		ok                   bool
   354  		paramList            []string
   355  		pauseNsAccumulator   uint64
   356  		sendPackedMetrics    bool
   357  		statKey              string
   358  		statValue            uint64
   359  		statsMap             map[string]uint64
   360  	)
   361  
   362  	runtime.ReadMemStats(&memStats)
   363  
   364  	metricsMap = make(map[string]uint64)
   365  
   366  	// General statistics.
   367  	metricsMap["go_runtime_MemStats_Alloc"] = memStats.Alloc
   368  	metricsMap["go_runtime_MemStats_TotalAlloc"] = memStats.TotalAlloc
   369  	metricsMap["go_runtime_MemStats_Sys"] = memStats.Sys
   370  	metricsMap["go_runtime_MemStats_Lookups"] = memStats.Lookups
   371  	metricsMap["go_runtime_MemStats_Mallocs"] = memStats.Mallocs
   372  	metricsMap["go_runtime_MemStats_Frees"] = memStats.Frees
   373  
   374  	// Main allocation heap statistics.
   375  	metricsMap["go_runtime_MemStats_HeapAlloc"] = memStats.HeapAlloc
   376  	metricsMap["go_runtime_MemStats_HeapSys"] = memStats.HeapSys
   377  	metricsMap["go_runtime_MemStats_HeapIdle"] = memStats.HeapIdle
   378  	metricsMap["go_runtime_MemStats_HeapInuse"] = memStats.HeapInuse
   379  	metricsMap["go_runtime_MemStats_HeapReleased"] = memStats.HeapReleased
   380  	metricsMap["go_runtime_MemStats_HeapObjects"] = memStats.HeapObjects
   381  
   382  	// Low-level fixed-size structure allocator statistics.
   383  	//	Inuse is bytes used now.
   384  	//	Sys is bytes obtained from system.
   385  	metricsMap["go_runtime_MemStats_StackInuse"] = memStats.StackInuse
   386  	metricsMap["go_runtime_MemStats_StackSys"] = memStats.StackSys
   387  	metricsMap["go_runtime_MemStats_MSpanInuse"] = memStats.MSpanInuse
   388  	metricsMap["go_runtime_MemStats_MSpanSys"] = memStats.MSpanSys
   389  	metricsMap["go_runtime_MemStats_MCacheInuse"] = memStats.MCacheInuse
   390  	metricsMap["go_runtime_MemStats_MCacheSys"] = memStats.MCacheSys
   391  	metricsMap["go_runtime_MemStats_BuckHashSys"] = memStats.BuckHashSys
   392  	metricsMap["go_runtime_MemStats_GCSys"] = memStats.GCSys
   393  	metricsMap["go_runtime_MemStats_OtherSys"] = memStats.OtherSys
   394  
   395  	// Garbage collector statistics (fixed portion).
   396  	metricsMap["go_runtime_MemStats_LastGC"] = memStats.LastGC
   397  	metricsMap["go_runtime_MemStats_PauseTotalNs"] = memStats.PauseTotalNs
   398  	metricsMap["go_runtime_MemStats_NumGC"] = uint64(memStats.NumGC)
   399  	metricsMap["go_runtime_MemStats_GCCPUPercentage"] = uint64(100.0 * memStats.GCCPUFraction)
   400  
   401  	// Garbage collector statistics (go_runtime_MemStats_PauseAverageNs).
   402  	if 0 == memStats.NumGC {
   403  		metricsMap["go_runtime_MemStats_PauseAverageNs"] = 0
   404  	} else {
   405  		pauseNsAccumulator = 0
   406  		if memStats.NumGC < 255 {
   407  			for i = 0; i < int(memStats.NumGC); i++ {
   408  				pauseNsAccumulator += memStats.PauseNs[i]
   409  			}
   410  			metricsMap["go_runtime_MemStats_PauseAverageNs"] = pauseNsAccumulator / uint64(memStats.NumGC)
   411  		} else {
   412  			for i = 0; i < 256; i++ {
   413  				pauseNsAccumulator += memStats.PauseNs[i]
   414  			}
   415  			metricsMap["go_runtime_MemStats_PauseAverageNs"] = pauseNsAccumulator / 256
   416  		}
   417  	}
   418  
   419  	statsMap = stats.Dump()
   420  
   421  	for statKey, statValue = range statsMap {
   422  		metricKey = strings.Replace(statKey, ".", "_", -1)
   423  		metricKey = strings.Replace(metricKey, "-", "_", -1)
   424  		metricsMap[metricKey] = statValue
   425  	}
   426  
   427  	acceptHeader = request.Header.Get("Accept")
   428  
   429  	if strings.Contains(acceptHeader, "application/json") {
   430  		formatResponseAsHTML = false
   431  		formatResponseAsJSON = true
   432  	} else if strings.Contains(acceptHeader, "text/html") {
   433  		formatResponseAsHTML = true
   434  		formatResponseAsJSON = true
   435  	} else if strings.Contains(acceptHeader, "text/plain") {
   436  		formatResponseAsHTML = false
   437  		formatResponseAsJSON = false
   438  	} else if strings.Contains(acceptHeader, "*/*") {
   439  		formatResponseAsHTML = false
   440  		formatResponseAsJSON = true
   441  	} else if strings.Contains(acceptHeader, "") {
   442  		formatResponseAsHTML = false
   443  		formatResponseAsJSON = true
   444  	} else {
   445  		responseWriter.WriteHeader(http.StatusNotAcceptable)
   446  		return
   447  	}
   448  
   449  	if formatResponseAsJSON {
   450  		metricsJSONPacked, _ = json.Marshal(metricsMap)
   451  		if formatResponseAsHTML {
   452  			responseWriter.Header().Set("Content-Type", "text/html")
   453  			responseWriter.WriteHeader(http.StatusOK)
   454  
   455  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(metricsTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, utils.ByteSliceToString(metricsJSONPacked))))
   456  		} else {
   457  			responseWriter.Header().Set("Content-Type", "application/json")
   458  			responseWriter.WriteHeader(http.StatusOK)
   459  
   460  			paramList, ok = request.URL.Query()["compact"]
   461  			if ok {
   462  				if 0 == len(paramList) {
   463  					sendPackedMetrics = false
   464  				} else {
   465  					sendPackedMetrics = !((paramList[0] == "") || (paramList[0] == "0") || (paramList[0] == "false"))
   466  				}
   467  			} else {
   468  				sendPackedMetrics = false
   469  			}
   470  
   471  			if sendPackedMetrics {
   472  				_, _ = responseWriter.Write(metricsJSONPacked)
   473  			} else {
   474  				json.Indent(&metricsJSON, metricsJSONPacked, "", "\t")
   475  				_, _ = responseWriter.Write(metricsJSON.Bytes())
   476  				_, _ = responseWriter.Write([]byte("\n"))
   477  			}
   478  		}
   479  	} else {
   480  		metricsLLRB = sortedmap.NewLLRBTree(sortedmap.CompareString, nil)
   481  
   482  		for metricKey, metricValueAsUint64 = range metricsMap {
   483  			metricValueAsString = fmt.Sprintf("%v", metricValueAsUint64)
   484  			ok, err = metricsLLRB.Put(metricKey, metricValueAsString)
   485  			if nil != err {
   486  				err = fmt.Errorf("metricsLLRB.Put(%v, %v) failed: %v", metricKey, metricValueAsString, err)
   487  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   488  			}
   489  			if !ok {
   490  				err = fmt.Errorf("metricsLLRB.Put(%v, %v) returned ok == false", metricKey, metricValueAsString)
   491  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   492  			}
   493  		}
   494  
   495  		sortedTwoColumnResponseWriter(metricsLLRB, responseWriter)
   496  	}
   497  }
   498  
   499  func doGetOfStats(responseWriter http.ResponseWriter, request *http.Request) {
   500  
   501  	responseWriter.Header().Set("Content-Type", "text/plain")
   502  	responseWriter.WriteHeader(http.StatusOK)
   503  
   504  	_, _ = responseWriter.Write([]byte(bucketstats.SprintStats(bucketstats.StatFormatParsable1, "*", "*")))
   505  }
   506  
   507  func doGetOfArmDisarmTrigger(responseWriter http.ResponseWriter, request *http.Request) {
   508  	var (
   509  		availableTriggers []string
   510  		err               error
   511  		haltTriggerString string
   512  		ok                bool
   513  		triggersLLRB      sortedmap.LLRBTree
   514  	)
   515  
   516  	responseWriter.Header().Set("Content-Type", "text/html")
   517  	responseWriter.WriteHeader(http.StatusOK)
   518  
   519  	_, _ = responseWriter.Write([]byte("<!DOCTYPE html>\n"))
   520  	_, _ = responseWriter.Write([]byte("<html lang=\"en\">\n"))
   521  	_, _ = responseWriter.Write([]byte("  <head>\n"))
   522  	_, _ = responseWriter.Write([]byte(fmt.Sprintf("    <title>Trigger Arm/Disarm Page</title>\n")))
   523  	_, _ = responseWriter.Write([]byte("  </head>\n"))
   524  	_, _ = responseWriter.Write([]byte("  <body>\n"))
   525  	_, _ = responseWriter.Write([]byte("    <form method=\"post\" action=\"/arm-disarm-trigger\">\n"))
   526  	_, _ = responseWriter.Write([]byte("      <select name=\"haltLabelString\">\n"))
   527  	_, _ = responseWriter.Write([]byte("        <option value=\"\">-- select one --</option>\n"))
   528  
   529  	availableTriggers = halter.List()
   530  
   531  	triggersLLRB = sortedmap.NewLLRBTree(sortedmap.CompareString, nil)
   532  
   533  	for _, haltTriggerString = range availableTriggers {
   534  		ok, err = triggersLLRB.Put(haltTriggerString, true)
   535  		if nil != err {
   536  			err = fmt.Errorf("triggersLLRB.Put(%v, true) failed: %v", haltTriggerString, err)
   537  			logger.Fatalf("HTTP Server Logic Error: %v", err)
   538  		}
   539  		if !ok {
   540  			err = fmt.Errorf("triggersLLRB.Put(%v, true) returned ok == false", haltTriggerString)
   541  			logger.Fatalf("HTTP Server Logic Error: %v", err)
   542  		}
   543  		_, _ = responseWriter.Write([]byte(fmt.Sprintf("        <option value=\"%v\">%v</option>\n", haltTriggerString, haltTriggerString)))
   544  	}
   545  
   546  	_, _ = responseWriter.Write([]byte("      </select>\n"))
   547  	_, _ = responseWriter.Write([]byte("      <input type=\"number\" name=\"haltAfterCount\" min=\"0\" max=\"4294967295\" required>\n"))
   548  	_, _ = responseWriter.Write([]byte("      <input type=\"submit\">\n"))
   549  	_, _ = responseWriter.Write([]byte("    </form>\n"))
   550  	_, _ = responseWriter.Write([]byte("  </body>\n"))
   551  	_, _ = responseWriter.Write([]byte("</html>\n"))
   552  }
   553  
   554  func doGetOfTrigger(responseWriter http.ResponseWriter, request *http.Request) {
   555  	var (
   556  		triggerAllArmedOrDisarmedActiveString string
   557  		armedTriggers                         map[string]uint32
   558  		availableTriggers                     []string
   559  		err                                   error
   560  		haltTriggerArmedStateAsBool           bool
   561  		haltTriggerArmedStateAsString         string
   562  		haltTriggerCount                      uint32
   563  		haltTriggerString                     string
   564  		i                                     int
   565  		lenTriggersLLRB                       int
   566  		numPathParts                          int
   567  		key                                   sortedmap.Key
   568  		ok                                    bool
   569  		pathSplit                             []string
   570  		triggersLLRB                          sortedmap.LLRBTree
   571  		value                                 sortedmap.Value
   572  	)
   573  
   574  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
   575  	//                                                  pathSplit[1] should be "trigger" based on how we got here
   576  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
   577  
   578  	numPathParts = len(pathSplit) - 1
   579  	if "" == pathSplit[numPathParts] {
   580  		numPathParts--
   581  	}
   582  
   583  	if "trigger" != pathSplit[1] {
   584  		responseWriter.WriteHeader(http.StatusNotFound)
   585  		return
   586  	}
   587  
   588  	switch numPathParts {
   589  	case 1:
   590  		// Form: /trigger[?armed={true|false}]
   591  
   592  		haltTriggerArmedStateAsString = request.FormValue("armed")
   593  
   594  		if "" == haltTriggerArmedStateAsString {
   595  			triggerAllArmedOrDisarmedActiveString = triggerAllActive
   596  			armedTriggers = halter.Dump()
   597  			availableTriggers = halter.List()
   598  
   599  			triggersLLRB = sortedmap.NewLLRBTree(sortedmap.CompareString, nil)
   600  
   601  			for _, haltTriggerString = range availableTriggers {
   602  				haltTriggerCount, ok = armedTriggers[haltTriggerString]
   603  				if !ok {
   604  					haltTriggerCount = 0
   605  				}
   606  				ok, err = triggersLLRB.Put(haltTriggerString, haltTriggerCount)
   607  				if nil != err {
   608  					err = fmt.Errorf("triggersLLRB.Put(%v, %v) failed: %v", haltTriggerString, haltTriggerCount, err)
   609  					logger.Fatalf("HTTP Server Logic Error: %v", err)
   610  				}
   611  				if !ok {
   612  					err = fmt.Errorf("triggersLLRB.Put(%v, %v) returned ok == false", haltTriggerString, haltTriggerCount)
   613  					logger.Fatalf("HTTP Server Logic Error: %v", err)
   614  				}
   615  			}
   616  		} else {
   617  			haltTriggerArmedStateAsBool, err = strconv.ParseBool(haltTriggerArmedStateAsString)
   618  			if nil == err {
   619  				triggersLLRB = sortedmap.NewLLRBTree(sortedmap.CompareString, nil)
   620  
   621  				if haltTriggerArmedStateAsBool {
   622  					triggerAllArmedOrDisarmedActiveString = triggerArmedActive
   623  					armedTriggers = halter.Dump()
   624  					for haltTriggerString, haltTriggerCount = range armedTriggers {
   625  						ok, err = triggersLLRB.Put(haltTriggerString, haltTriggerCount)
   626  						if nil != err {
   627  							err = fmt.Errorf("triggersLLRB.Put(%v, %v) failed: %v", haltTriggerString, haltTriggerCount, err)
   628  							logger.Fatalf("HTTP Server Logic Error: %v", err)
   629  						}
   630  						if !ok {
   631  							err = fmt.Errorf("triggersLLRB.Put(%v, %v) returned ok == false", haltTriggerString, haltTriggerCount)
   632  							logger.Fatalf("HTTP Server Logic Error: %v", err)
   633  						}
   634  					}
   635  				} else {
   636  					triggerAllArmedOrDisarmedActiveString = triggerDisarmedActive
   637  					armedTriggers = halter.Dump()
   638  					availableTriggers = halter.List()
   639  
   640  					for _, haltTriggerString = range availableTriggers {
   641  						_, ok = armedTriggers[haltTriggerString]
   642  						if !ok {
   643  							ok, err = triggersLLRB.Put(haltTriggerString, uint32(0))
   644  							if nil != err {
   645  								err = fmt.Errorf("triggersLLRB.Put(%v, %v) failed: %v", haltTriggerString, 0, err)
   646  								logger.Fatalf("HTTP Server Logic Error: %v", err)
   647  							}
   648  							if !ok {
   649  								err = fmt.Errorf("triggersLLRB.Put(%v, %v) returned ok == false", haltTriggerString, 0)
   650  								logger.Fatalf("HTTP Server Logic Error: %v", err)
   651  							}
   652  						}
   653  					}
   654  				}
   655  			} else {
   656  				responseWriter.WriteHeader(http.StatusBadRequest)
   657  			}
   658  		}
   659  
   660  		responseWriter.Header().Set("Content-Type", "text/html")
   661  		responseWriter.WriteHeader(http.StatusOK)
   662  
   663  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(triggerTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort)))
   664  		_, _ = responseWriter.Write([]byte(triggerAllArmedOrDisarmedActiveString))
   665  		_, _ = responseWriter.Write([]byte(triggerTableTop))
   666  
   667  		lenTriggersLLRB, err = triggersLLRB.Len()
   668  		if nil != err {
   669  			err = fmt.Errorf("triggersLLRB.Len()) failed: %v", err)
   670  			logger.Fatalf("HTTP Server Logic Error: %v", err)
   671  		}
   672  
   673  		for i = 0; i < lenTriggersLLRB; i++ {
   674  			key, value, ok, err = triggersLLRB.GetByIndex(i)
   675  			if nil != err {
   676  				err = fmt.Errorf("triggersLLRB.GetByIndex(%v) failed: %v", i, err)
   677  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   678  			}
   679  			if !ok {
   680  				err = fmt.Errorf("triggersLLRB.GetByIndex(%v) returned ok == false", i)
   681  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   682  			}
   683  
   684  			haltTriggerString = key.(string)
   685  			haltTriggerCount = value.(uint32)
   686  
   687  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(triggerTableRowTemplate, haltTriggerString, haltTriggerCount)))
   688  		}
   689  
   690  		_, _ = responseWriter.Write([]byte(triggerBottom))
   691  	case 2:
   692  		// Form: /trigger/<trigger-name>
   693  
   694  		haltTriggerString = pathSplit[2]
   695  
   696  		haltTriggerCount, err = halter.Stat(haltTriggerString)
   697  		if nil == err {
   698  			responseWriter.Header().Set("Content-Type", "text/plain")
   699  			responseWriter.WriteHeader(http.StatusOK)
   700  
   701  			_, _ = responseWriter.Write([]byte(fmt.Sprintf("%v\n", haltTriggerCount)))
   702  		} else {
   703  			responseWriter.WriteHeader(http.StatusNotFound)
   704  		}
   705  	default:
   706  		responseWriter.WriteHeader(http.StatusNotFound)
   707  	}
   708  }
   709  
   710  func doGetOfVolume(responseWriter http.ResponseWriter, request *http.Request) {
   711  	var (
   712  		acceptHeader            string
   713  		err                     error
   714  		formatResponseAsJSON    bool
   715  		formatResponseCompactly bool
   716  		numPathParts            int
   717  		ok                      bool
   718  		paramList               []string
   719  		pathSplit               []string
   720  		percentRange            string
   721  		performValidation       bool
   722  		requestState            *requestStateStruct
   723  		startNonceAsString      string
   724  		startNonceAsUint64      uint64
   725  		volumeAsValue           sortedmap.Value
   726  		volumeList              []string
   727  		volumeListIndex         int
   728  		volumeListJSON          bytes.Buffer
   729  		volumeListJSONPacked    []byte
   730  		volumeListLen           int
   731  		volumeName              string
   732  		volumeNameAsKey         sortedmap.Key
   733  	)
   734  
   735  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
   736  	//                                                  pathSplit[1] should be "volume" based on how we got here
   737  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
   738  
   739  	numPathParts = len(pathSplit) - 1
   740  	if "" == pathSplit[numPathParts] {
   741  		numPathParts--
   742  	}
   743  
   744  	if "volume" != pathSplit[1] {
   745  		responseWriter.WriteHeader(http.StatusNotFound)
   746  		return
   747  	}
   748  
   749  	switch numPathParts {
   750  	case 1:
   751  		// Form: /volume
   752  	case 2:
   753  		responseWriter.WriteHeader(http.StatusNotFound)
   754  		return
   755  	case 3:
   756  		// Form: /volume/<volume-name>/extent-map
   757  		// Form: /volume/<volume-name>/fsck-job
   758  		// Form: /volume/<volume-name>/layout-report
   759  		// Form: /volume/<volume-name>/lease-report
   760  		// Form: /volume/<volume-name>/meta-defrag
   761  		// Form: /volume/<volume-name>/scrub-job
   762  		// Form: /volume/<volume-name>/snapshot
   763  	case 4:
   764  		// Form: /volume/<volume-name>/defrag/<basename>
   765  		// Form: /volume/<volume-name>/extent-map/<basename>
   766  		// Form: /volume/<volume-name>/find-subdir-inodes/<DirInodeNumberAs16HexDigits>
   767  		// Form: /volume/<volume-name>/fsck-job/<job-id>
   768  		// Form: /volume/<volume-name>/meta-defrag/<BPlusTreeType>
   769  		// Form: /volume/<volume-name>/scrub-job/<job-id>
   770  	default:
   771  		// Form: /volume/<volume-name>/defrag/<dir>/.../<basename>
   772  		// Form: /volume/<volume-name>/extent-map/<dir>/.../<basename>
   773  		// Form: /volume/<volume-name>/find-dir-inode/<dir>/.../<basename>
   774  	}
   775  
   776  	acceptHeader = request.Header.Get("Accept")
   777  
   778  	if strings.Contains(acceptHeader, "application/json") {
   779  		formatResponseAsJSON = true
   780  	} else if strings.Contains(acceptHeader, "text/html") {
   781  		formatResponseAsJSON = false
   782  	} else if strings.Contains(acceptHeader, "*/*") {
   783  		formatResponseAsJSON = true
   784  	} else if strings.Contains(acceptHeader, "") {
   785  		formatResponseAsJSON = true
   786  	} else {
   787  		responseWriter.WriteHeader(http.StatusNotAcceptable)
   788  		return
   789  	}
   790  
   791  	if formatResponseAsJSON {
   792  		paramList, ok = request.URL.Query()["compact"]
   793  		if ok {
   794  			if 0 == len(paramList) {
   795  				formatResponseCompactly = false
   796  			} else {
   797  				formatResponseCompactly = !((paramList[0] == "") || (paramList[0] == "0") || (paramList[0] == "false"))
   798  			}
   799  		} else {
   800  			formatResponseCompactly = false
   801  		}
   802  	}
   803  
   804  	paramList, ok = request.URL.Query()["validate"]
   805  	if ok {
   806  		if 0 == len(paramList) {
   807  			performValidation = false
   808  		} else {
   809  			performValidation = !((paramList[0] == "") || (paramList[0] == "0") || (paramList[0] == "false"))
   810  		}
   811  	} else {
   812  		performValidation = false
   813  	}
   814  
   815  	paramList, ok = request.URL.Query()["range"]
   816  	if ok {
   817  		if 0 == len(paramList) {
   818  			percentRange = "0-100"
   819  		} else {
   820  			percentRange = paramList[0]
   821  			if "" == percentRange {
   822  				percentRange = "0-100"
   823  			}
   824  		}
   825  	} else {
   826  		percentRange = "0-100"
   827  	}
   828  
   829  	paramList, ok = request.URL.Query()["start"]
   830  	if ok {
   831  		if 0 == len(paramList) {
   832  			startNonceAsUint64 = uint64(0)
   833  		} else {
   834  			startNonceAsString = paramList[0]
   835  			startNonceAsUint64, err = strconv.ParseUint(startNonceAsString, 16, 64)
   836  			if nil != err {
   837  				responseWriter.WriteHeader(http.StatusBadRequest)
   838  				return
   839  			}
   840  		}
   841  	} else {
   842  		startNonceAsUint64 = uint64(0)
   843  	}
   844  
   845  	if 1 == numPathParts {
   846  		volumeListLen, err = globals.volumeLLRB.Len()
   847  		if nil != err {
   848  			logger.Fatalf("HTTP Server Logic Error: %v", err)
   849  		}
   850  
   851  		volumeList = make([]string, 0, volumeListLen)
   852  		for volumeListIndex = 0; volumeListIndex < volumeListLen; volumeListIndex++ {
   853  			// GetByIndex(index int) (key Key, value Value, ok bool, err error)
   854  			volumeNameAsKey, _, ok, err = globals.volumeLLRB.GetByIndex(volumeListIndex)
   855  			if nil != err {
   856  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   857  			}
   858  			if !ok {
   859  				err = fmt.Errorf("httpserver.doGetOfVolume() indexing globals.volumeLLRB failed")
   860  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   861  			}
   862  
   863  			volumeName = volumeNameAsKey.(string)
   864  			volumeList = append(volumeList, volumeName)
   865  		}
   866  
   867  		if formatResponseAsJSON {
   868  			responseWriter.Header().Set("Content-Type", "application/json")
   869  			responseWriter.WriteHeader(http.StatusOK)
   870  
   871  			volumeListJSONPacked, err = json.Marshal(volumeList)
   872  			if nil != err {
   873  				logger.Fatalf("HTTP Server Logic Error: %v", err)
   874  			}
   875  
   876  			if formatResponseCompactly {
   877  				_, _ = responseWriter.Write(volumeListJSONPacked)
   878  			} else {
   879  				json.Indent(&volumeListJSON, volumeListJSONPacked, "", "\t")
   880  				_, _ = responseWriter.Write(volumeListJSON.Bytes())
   881  				_, _ = responseWriter.Write([]byte("\n"))
   882  			}
   883  		} else {
   884  			responseWriter.Header().Set("Content-Type", "text/html")
   885  			responseWriter.WriteHeader(http.StatusOK)
   886  
   887  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(volumeListTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort)))
   888  
   889  			for volumeListIndex, volumeName = range volumeList {
   890  				_, _ = responseWriter.Write([]byte(fmt.Sprintf(volumeListPerVolumeTemplate, volumeName)))
   891  			}
   892  
   893  			_, _ = responseWriter.Write([]byte(volumeListBottom))
   894  		}
   895  
   896  		return
   897  	}
   898  
   899  	// If we reach here, numPathParts is at least 3
   900  
   901  	volumeName = pathSplit[2]
   902  
   903  	volumeAsValue, ok, err = globals.volumeLLRB.GetByKey(volumeName)
   904  	if nil != err {
   905  		logger.Fatalf("HTTP Server Logic Error: %v", err)
   906  	}
   907  	if !ok {
   908  		responseWriter.WriteHeader(http.StatusNotFound)
   909  		return
   910  	}
   911  
   912  	requestState = &requestStateStruct{
   913  		pathSplit:               pathSplit,
   914  		numPathParts:            numPathParts,
   915  		formatResponseAsJSON:    formatResponseAsJSON,
   916  		formatResponseCompactly: formatResponseCompactly,
   917  		performValidation:       performValidation,
   918  		percentRange:            percentRange,
   919  		startNonce:              startNonceAsUint64,
   920  		volume:                  volumeAsValue.(*volumeStruct),
   921  	}
   922  
   923  	requestState.pathSplit = pathSplit
   924  	requestState.numPathParts = numPathParts
   925  	requestState.formatResponseAsJSON = formatResponseAsJSON
   926  	requestState.formatResponseCompactly = formatResponseCompactly
   927  
   928  	switch pathSplit[3] {
   929  	case "defrag":
   930  		doDefrag(responseWriter, request, requestState)
   931  
   932  	case "find-dir-inode":
   933  		doFindDirInode(responseWriter, request, requestState)
   934  
   935  	case "find-subdir-inodes":
   936  		doFindSubDirInodes(responseWriter, request, requestState)
   937  
   938  	case "extent-map":
   939  		doExtentMap(responseWriter, request, requestState)
   940  
   941  	case "fsck-job":
   942  		doJob(fsckJobType, responseWriter, request, requestState)
   943  
   944  	case "layout-report":
   945  		doLayoutReport(responseWriter, request, requestState)
   946  
   947  	case "lease-report":
   948  		doLeaseReport(responseWriter, request, requestState)
   949  
   950  	case "meta-defrag":
   951  		doMetaDefrag(responseWriter, request, requestState)
   952  
   953  	case "scrub-job":
   954  		doJob(scrubJobType, responseWriter, request, requestState)
   955  
   956  	case "snapshot":
   957  		doGetOfSnapShot(responseWriter, request, requestState)
   958  
   959  	default:
   960  		responseWriter.WriteHeader(http.StatusNotFound)
   961  		return
   962  	}
   963  
   964  	return
   965  }
   966  
   967  func doFindDirInode(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
   968  	var (
   969  		parentDirEntryBasename string
   970  		dirInodeNumber         inode.InodeNumber
   971  		err                    error
   972  		parentDirInodeNumber   inode.InodeNumber
   973  		pathSplitIndex         int
   974  		pathSplitIndexMax      int
   975  	)
   976  
   977  	if 3 > requestState.numPathParts {
   978  		responseWriter.WriteHeader(http.StatusNotFound)
   979  		return
   980  	}
   981  
   982  	if "" == requestState.pathSplit[len(requestState.pathSplit)-1] {
   983  		pathSplitIndexMax = len(requestState.pathSplit) - 2
   984  	} else {
   985  		pathSplitIndexMax = len(requestState.pathSplit) - 1
   986  	}
   987  
   988  	parentDirInodeNumber = inode.RootDirInodeNumber
   989  	parentDirEntryBasename = "."
   990  	dirInodeNumber = inode.RootDirInodeNumber
   991  
   992  	for pathSplitIndex = 4; pathSplitIndex <= pathSplitIndexMax; pathSplitIndex++ {
   993  		parentDirInodeNumber = dirInodeNumber
   994  		parentDirEntryBasename = requestState.pathSplit[pathSplitIndex]
   995  
   996  		dirInodeNumber, err = requestState.volume.fsVolumeHandle.Lookup(inode.InodeRootUserID, inode.InodeGroupID(0), nil, parentDirInodeNumber, parentDirEntryBasename)
   997  		if nil != err {
   998  			responseWriter.WriteHeader(http.StatusNotFound)
   999  			return
  1000  		}
  1001  	}
  1002  
  1003  	responseWriter.Header().Set("Content-Type", "text/plain")
  1004  	responseWriter.WriteHeader(http.StatusOK)
  1005  
  1006  	_, _ = responseWriter.Write([]byte(fmt.Sprintf("(%016X) %s => %016X\n", parentDirInodeNumber, parentDirEntryBasename, dirInodeNumber)))
  1007  }
  1008  
  1009  func doFindSubDirInodes(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1010  	var (
  1011  		dirInodeNumber          inode.InodeNumber
  1012  		dirInodeNumberAsUint64  uint64
  1013  		err                     error
  1014  		lastInodeNumberAsUint64 uint64
  1015  		nextInodeNumberAsUint64 uint64
  1016  		ok                      bool
  1017  		subDirInodeNumber       inode.InodeNumber
  1018  		subDirInodeNumbers      []inode.InodeNumber
  1019  		subDirParentInodeNumber inode.InodeNumber
  1020  	)
  1021  
  1022  	if 4 != requestState.numPathParts {
  1023  		responseWriter.WriteHeader(http.StatusNotFound)
  1024  		return
  1025  	}
  1026  
  1027  	dirInodeNumberAsUint64, err = strconv.ParseUint(requestState.pathSplit[4], 16, 64)
  1028  	if nil != err {
  1029  		responseWriter.WriteHeader(http.StatusNotFound)
  1030  		return
  1031  	}
  1032  	dirInodeNumber = inode.InodeNumber(dirInodeNumberAsUint64)
  1033  
  1034  	globals.Unlock()
  1035  
  1036  	subDirInodeNumbers = make([]inode.InodeNumber, 0)
  1037  
  1038  	lastInodeNumberAsUint64 = requestState.startNonce
  1039  
  1040  	for {
  1041  		nextInodeNumberAsUint64, ok, err = requestState.volume.headhunterVolumeHandle.NextInodeNumber(lastInodeNumberAsUint64)
  1042  		if nil != err {
  1043  			logger.FatalfWithError(err, "Unexpected failure walking Inode Table")
  1044  		}
  1045  
  1046  		if !ok {
  1047  			break
  1048  		}
  1049  
  1050  		subDirInodeNumber = inode.InodeNumber(nextInodeNumberAsUint64)
  1051  
  1052  		subDirParentInodeNumber, err = requestState.volume.fsVolumeHandle.Lookup(inode.InodeRootUserID, inode.InodeGroupID(0), nil, subDirInodeNumber, "..")
  1053  		if (nil == err) && (subDirParentInodeNumber == dirInodeNumber) {
  1054  			subDirInodeNumbers = append(subDirInodeNumbers, subDirInodeNumber)
  1055  		}
  1056  
  1057  		lastInodeNumberAsUint64 = nextInodeNumberAsUint64
  1058  	}
  1059  
  1060  	responseWriter.Header().Set("Content-Type", "text/plain")
  1061  	responseWriter.WriteHeader(http.StatusOK)
  1062  
  1063  	for _, subDirInodeNumber = range subDirInodeNumbers {
  1064  		_, _ = responseWriter.Write([]byte(fmt.Sprintf("%016X\n", subDirInodeNumber)))
  1065  	}
  1066  
  1067  	globals.Lock()
  1068  }
  1069  
  1070  func doMetaDefrag(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1071  	var (
  1072  		bPlusTreeType              string
  1073  		bPlusTreeTypeSlice         []string
  1074  		err                        error
  1075  		percentRangeSplit          []string
  1076  		percentRangeStartAsFloat64 float64
  1077  		percentRangeStopAsFloat64  float64
  1078  	)
  1079  
  1080  	if 3 == requestState.numPathParts {
  1081  		bPlusTreeTypeSlice = []string{"InodeRecBPlusTree", "LogSegmentRecBPlusTree", "BPlusTreeObjectBPlusTree", "CreatedObjectsBPlusTree", "DeletedObjectsBPlusTree"}
  1082  	} else if 4 == requestState.numPathParts {
  1083  		bPlusTreeTypeSlice = []string{requestState.pathSplit[4]}
  1084  	} else {
  1085  		responseWriter.WriteHeader(http.StatusNotFound)
  1086  		return
  1087  	}
  1088  
  1089  	percentRangeSplit = strings.Split(requestState.percentRange, "-")
  1090  
  1091  	if 2 != len(percentRangeSplit) {
  1092  		responseWriter.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1093  		return
  1094  	}
  1095  
  1096  	percentRangeStartAsFloat64, err = strconv.ParseFloat(percentRangeSplit[0], 64)
  1097  	if (nil != err) || (0 > percentRangeStartAsFloat64) || (100 <= percentRangeStartAsFloat64) {
  1098  		responseWriter.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1099  		return
  1100  	}
  1101  
  1102  	percentRangeStopAsFloat64, err = strconv.ParseFloat(percentRangeSplit[1], 64)
  1103  	if (nil != err) || (percentRangeStartAsFloat64 >= percentRangeStopAsFloat64) || (100 < percentRangeStopAsFloat64) {
  1104  		responseWriter.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
  1105  		return
  1106  	}
  1107  
  1108  	for _, bPlusTreeType = range bPlusTreeTypeSlice {
  1109  		switch bPlusTreeType {
  1110  		case "InodeRecBPlusTree":
  1111  			err = requestState.volume.headhunterVolumeHandle.DefragmentMetadata(
  1112  				headhunter.InodeRecBPlusTree,
  1113  				percentRangeStartAsFloat64,
  1114  				percentRangeStopAsFloat64)
  1115  		case "LogSegmentRecBPlusTree":
  1116  			err = requestState.volume.headhunterVolumeHandle.DefragmentMetadata(
  1117  				headhunter.LogSegmentRecBPlusTree,
  1118  				percentRangeStartAsFloat64,
  1119  				percentRangeStopAsFloat64)
  1120  		case "BPlusTreeObjectBPlusTree":
  1121  			err = requestState.volume.headhunterVolumeHandle.DefragmentMetadata(
  1122  				headhunter.BPlusTreeObjectBPlusTree,
  1123  				percentRangeStartAsFloat64,
  1124  				percentRangeStopAsFloat64)
  1125  		case "CreatedObjectsBPlusTree":
  1126  			err = requestState.volume.headhunterVolumeHandle.DefragmentMetadata(
  1127  				headhunter.CreatedObjectsBPlusTree,
  1128  				percentRangeStartAsFloat64,
  1129  				percentRangeStopAsFloat64)
  1130  		case "DeletedObjectsBPlusTree":
  1131  			err = requestState.volume.headhunterVolumeHandle.DefragmentMetadata(
  1132  				headhunter.DeletedObjectsBPlusTree,
  1133  				percentRangeStartAsFloat64,
  1134  				percentRangeStopAsFloat64)
  1135  		default:
  1136  			responseWriter.WriteHeader(http.StatusNotFound)
  1137  			return
  1138  		}
  1139  
  1140  		if nil != err {
  1141  			logger.Fatalf("Call to %s.headhunterVolumeHandle.DefragmentMetadata(%s,,) failed: %v", requestState.volume.name, bPlusTreeType, err)
  1142  			responseWriter.WriteHeader(http.StatusInternalServerError)
  1143  			return
  1144  		}
  1145  	}
  1146  
  1147  	responseWriter.WriteHeader(http.StatusNoContent)
  1148  }
  1149  
  1150  func doDefrag(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1151  	var (
  1152  		alreadyInActiveDefragInodeNumberSet bool
  1153  		dirEntryInodeNumber                 inode.InodeNumber
  1154  		dirInodeNumber                      inode.InodeNumber
  1155  		err                                 error
  1156  		pathPartIndex                       int
  1157  	)
  1158  
  1159  	if 3 > requestState.numPathParts {
  1160  		err = fmt.Errorf("doDefrag() not passed enough requestState.numPathParts (%d)", requestState.numPathParts)
  1161  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1162  	}
  1163  
  1164  	dirEntryInodeNumber = inode.RootDirInodeNumber
  1165  	pathPartIndex = 3
  1166  
  1167  	for ; pathPartIndex < requestState.numPathParts; pathPartIndex++ {
  1168  		dirInodeNumber = dirEntryInodeNumber
  1169  
  1170  		dirEntryInodeNumber, err = requestState.volume.fsVolumeHandle.Lookup(inode.InodeRootUserID, inode.InodeGroupID(0), nil, dirInodeNumber, requestState.pathSplit[pathPartIndex+1])
  1171  		if nil != err {
  1172  			responseWriter.WriteHeader(http.StatusNotFound)
  1173  			return
  1174  		}
  1175  	}
  1176  
  1177  	_, alreadyInActiveDefragInodeNumberSet = requestState.volume.activeDefragInodeNumberSet[dirEntryInodeNumber]
  1178  	if alreadyInActiveDefragInodeNumberSet {
  1179  		responseWriter.WriteHeader(http.StatusConflict)
  1180  		return
  1181  	}
  1182  
  1183  	requestState.volume.activeDefragInodeNumberSet[dirEntryInodeNumber] = struct{}{}
  1184  
  1185  	globals.Unlock()
  1186  
  1187  	err = requestState.volume.fsVolumeHandle.DefragmentFile(inode.InodeRootUserID, inode.InodeGroupID(0), nil, dirEntryInodeNumber)
  1188  
  1189  	if nil == err {
  1190  		responseWriter.WriteHeader(http.StatusOK)
  1191  	} else {
  1192  		responseWriter.WriteHeader(http.StatusConflict)
  1193  	}
  1194  
  1195  	globals.Lock()
  1196  
  1197  	delete(requestState.volume.activeDefragInodeNumberSet, dirEntryInodeNumber)
  1198  }
  1199  
  1200  func doExtentMap(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1201  	var (
  1202  		dirEntryInodeNumber inode.InodeNumber
  1203  		dirInodeNumber      inode.InodeNumber
  1204  		err                 error
  1205  		extentMap           []ExtentMapElementStruct
  1206  		extentMapChunk      *inode.ExtentMapChunkStruct
  1207  		extentMapEntry      inode.ExtentMapEntryStruct
  1208  		extentMapJSONBuffer bytes.Buffer
  1209  		extentMapJSONPacked []byte
  1210  		path                string
  1211  		pathDoubleQuoted    string
  1212  		pathPartIndex       int
  1213  	)
  1214  
  1215  	if 3 > requestState.numPathParts {
  1216  		err = fmt.Errorf("doExtentMap() not passed enough requestState.numPathParts (%d)", requestState.numPathParts)
  1217  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1218  	}
  1219  
  1220  	if (3 == requestState.numPathParts) && requestState.formatResponseAsJSON {
  1221  		responseWriter.WriteHeader(http.StatusNotFound)
  1222  		return
  1223  	}
  1224  
  1225  	if 3 == requestState.numPathParts {
  1226  		responseWriter.Header().Set("Content-Type", "text/html")
  1227  		responseWriter.WriteHeader(http.StatusOK)
  1228  
  1229  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(extentMapTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name, "null", "null", "false")))
  1230  		return
  1231  	}
  1232  
  1233  	path = strings.Join(requestState.pathSplit[4:], "/")
  1234  	pathDoubleQuoted = "\"" + path + "\""
  1235  
  1236  	dirEntryInodeNumber = inode.RootDirInodeNumber
  1237  	pathPartIndex = 3
  1238  
  1239  	for ; pathPartIndex < requestState.numPathParts; pathPartIndex++ {
  1240  		dirInodeNumber = dirEntryInodeNumber
  1241  		dirEntryInodeNumber, err = requestState.volume.fsVolumeHandle.Lookup(inode.InodeRootUserID, inode.InodeGroupID(0), nil, dirInodeNumber, requestState.pathSplit[pathPartIndex+1])
  1242  		if nil != err {
  1243  			if requestState.formatResponseAsJSON {
  1244  				responseWriter.WriteHeader(http.StatusNotFound)
  1245  				return
  1246  			} else {
  1247  				responseWriter.Header().Set("Content-Type", "text/html")
  1248  				responseWriter.WriteHeader(http.StatusOK)
  1249  
  1250  				_, _ = responseWriter.Write([]byte(fmt.Sprintf(extentMapTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name, "null", pathDoubleQuoted, "true")))
  1251  				return
  1252  			}
  1253  		}
  1254  	}
  1255  
  1256  	extentMapChunk, err = requestState.volume.fsVolumeHandle.FetchExtentMapChunk(inode.InodeRootUserID, inode.InodeGroupID(0), nil, dirEntryInodeNumber, uint64(0), math.MaxInt64, int64(0))
  1257  	if nil != err {
  1258  		if requestState.formatResponseAsJSON {
  1259  			responseWriter.WriteHeader(http.StatusNotFound)
  1260  			return
  1261  		} else {
  1262  			responseWriter.Header().Set("Content-Type", "text/html")
  1263  			responseWriter.WriteHeader(http.StatusOK)
  1264  
  1265  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(extentMapTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name, "null", pathDoubleQuoted, "true")))
  1266  			return
  1267  		}
  1268  	}
  1269  
  1270  	extentMap = make([]ExtentMapElementStruct, 0, len(extentMapChunk.ExtentMapEntry))
  1271  
  1272  	for _, extentMapEntry = range extentMapChunk.ExtentMapEntry {
  1273  		extentMap = append(extentMap, ExtentMapElementStruct{
  1274  			FileOffset:    extentMapEntry.FileOffset,
  1275  			ContainerName: extentMapEntry.ContainerName,
  1276  			ObjectName:    extentMapEntry.ObjectName,
  1277  			ObjectOffset:  extentMapEntry.LogSegmentOffset,
  1278  			Length:        extentMapEntry.Length,
  1279  		})
  1280  	}
  1281  
  1282  	extentMapJSONPacked, err = json.Marshal(extentMap)
  1283  	if nil != err {
  1284  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1285  	}
  1286  
  1287  	if requestState.formatResponseAsJSON {
  1288  		responseWriter.Header().Set("Content-Type", "application/json")
  1289  		responseWriter.WriteHeader(http.StatusOK)
  1290  
  1291  		if requestState.formatResponseCompactly {
  1292  			_, _ = responseWriter.Write(extentMapJSONPacked)
  1293  		} else {
  1294  			json.Indent(&extentMapJSONBuffer, extentMapJSONPacked, "", "\t")
  1295  			_, _ = responseWriter.Write(extentMapJSONBuffer.Bytes())
  1296  			_, _ = responseWriter.Write([]byte("\n"))
  1297  		}
  1298  	} else {
  1299  		responseWriter.Header().Set("Content-Type", "text/html")
  1300  		responseWriter.WriteHeader(http.StatusOK)
  1301  
  1302  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(extentMapTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name, utils.ByteSliceToString(extentMapJSONPacked), pathDoubleQuoted, "false")))
  1303  	}
  1304  }
  1305  
  1306  func doJob(jobType jobTypeType, responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1307  	var (
  1308  		err                     error
  1309  		formatResponseAsJSON    bool
  1310  		formatResponseCompactly bool
  1311  		inactive                bool
  1312  		job                     *jobStruct
  1313  		jobAsValue              sortedmap.Value
  1314  		jobErrorList            []string
  1315  		jobID                   uint64
  1316  		jobIDAsKey              sortedmap.Key
  1317  		jobPerJobTemplate       string
  1318  		jobsIDListJSONBuffer    bytes.Buffer
  1319  		jobsIDListJSONPacked    []byte
  1320  		jobStatusJSONBuffer     bytes.Buffer
  1321  		jobStatusJSONPacked     []byte
  1322  		jobStatusJSONStruct     *JobStatusJSONPackedStruct
  1323  		jobsCount               int
  1324  		jobsIDList              []uint64
  1325  		jobsIndex               int
  1326  		ok                      bool
  1327  		numPathParts            int
  1328  		pathSplit               []string
  1329  		volume                  *volumeStruct
  1330  		volumeName              string
  1331  	)
  1332  
  1333  	if limitJobType <= jobType {
  1334  		err = fmt.Errorf("httpserver.doJob(jobtype==%v,,,) called for invalid jobType", jobType)
  1335  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1336  	}
  1337  
  1338  	volume = requestState.volume
  1339  	pathSplit = requestState.pathSplit
  1340  	numPathParts = requestState.numPathParts
  1341  	formatResponseAsJSON = requestState.formatResponseAsJSON
  1342  	formatResponseCompactly = requestState.formatResponseCompactly
  1343  
  1344  	volumeName = volume.name
  1345  	volume.Lock()
  1346  
  1347  	markJobsCompletedIfNoLongerActiveWhileLocked(volume)
  1348  
  1349  	if 3 == numPathParts {
  1350  		switch jobType {
  1351  		case fsckJobType:
  1352  			jobsCount, err = volume.fsckJobs.Len()
  1353  		case scrubJobType:
  1354  			jobsCount, err = volume.scrubJobs.Len()
  1355  		}
  1356  		if nil != err {
  1357  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  1358  		}
  1359  
  1360  		inactive = (nil == volume.fsckActiveJob) && (nil == volume.scrubActiveJob)
  1361  
  1362  		volume.Unlock()
  1363  
  1364  		if formatResponseAsJSON {
  1365  			jobsIDList = make([]uint64, 0, jobsCount)
  1366  
  1367  			for jobsIndex = jobsCount - 1; jobsIndex >= 0; jobsIndex-- {
  1368  				switch jobType {
  1369  				case fsckJobType:
  1370  					jobIDAsKey, _, ok, err = volume.fsckJobs.GetByIndex(jobsIndex)
  1371  				case scrubJobType:
  1372  					jobIDAsKey, _, ok, err = volume.scrubJobs.GetByIndex(jobsIndex)
  1373  				}
  1374  				if nil != err {
  1375  					logger.Fatalf("HTTP Server Logic Error: %v", err)
  1376  				}
  1377  				if !ok {
  1378  					switch jobType {
  1379  					case fsckJobType:
  1380  						err = fmt.Errorf("httpserver.doGetOfVolume() indexing volume.fsckJobs failed")
  1381  					case scrubJobType:
  1382  						err = fmt.Errorf("httpserver.doGetOfVolume() indexing volume.scrubJobs failed")
  1383  					}
  1384  					logger.Fatalf("HTTP Server Logic Error: %v", err)
  1385  				}
  1386  				jobID = jobIDAsKey.(uint64)
  1387  
  1388  				jobsIDList = append(jobsIDList, jobID)
  1389  			}
  1390  
  1391  			responseWriter.Header().Set("Content-Type", "application/json")
  1392  			responseWriter.WriteHeader(http.StatusOK)
  1393  
  1394  			jobsIDListJSONPacked, err = json.Marshal(jobsIDList)
  1395  			if nil != err {
  1396  				logger.Fatalf("HTTP Server Logic Error: %v", err)
  1397  			}
  1398  
  1399  			if formatResponseCompactly {
  1400  				_, _ = responseWriter.Write(jobsIDListJSONPacked)
  1401  			} else {
  1402  				json.Indent(&jobsIDListJSONBuffer, jobsIDListJSONPacked, "", "\t")
  1403  				_, _ = responseWriter.Write(jobsIDListJSONBuffer.Bytes())
  1404  				_, _ = responseWriter.Write([]byte("\n"))
  1405  			}
  1406  		} else {
  1407  			responseWriter.Header().Set("Content-Type", "text/html")
  1408  			responseWriter.WriteHeader(http.StatusOK)
  1409  
  1410  			switch jobType {
  1411  			case fsckJobType:
  1412  				_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, volumeName, "FSCK")))
  1413  			case scrubJobType:
  1414  				_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, volumeName, "SCRUB")))
  1415  			}
  1416  
  1417  			for jobsIndex = jobsCount - 1; jobsIndex >= 0; jobsIndex-- {
  1418  				switch jobType {
  1419  				case fsckJobType:
  1420  					jobIDAsKey, jobAsValue, ok, err = volume.fsckJobs.GetByIndex(jobsIndex)
  1421  				case scrubJobType:
  1422  					jobIDAsKey, jobAsValue, ok, err = volume.scrubJobs.GetByIndex(jobsIndex)
  1423  				}
  1424  				if nil != err {
  1425  					logger.Fatalf("HTTP Server Logic Error: %v", err)
  1426  				}
  1427  				if !ok {
  1428  					switch jobType {
  1429  					case fsckJobType:
  1430  						err = fmt.Errorf("httpserver.doGetOfVolume() indexing volume.fsckJobs failed")
  1431  					case scrubJobType:
  1432  						err = fmt.Errorf("httpserver.doGetOfVolume() indexing volume.scrubJobs failed")
  1433  					}
  1434  					logger.Fatalf("HTTP Server Logic Error: %v", err)
  1435  				}
  1436  
  1437  				jobID = jobIDAsKey.(uint64)
  1438  				job = jobAsValue.(*jobStruct)
  1439  
  1440  				if jobRunning == job.state {
  1441  					switch jobType {
  1442  					case fsckJobType:
  1443  						_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsPerRunningJobTemplate, jobID, job.startTime.Format(time.RFC3339), volumeName, "fsck")))
  1444  					case scrubJobType:
  1445  						_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsPerRunningJobTemplate, jobID, job.startTime.Format(time.RFC3339), volumeName, "scrub")))
  1446  					}
  1447  				} else {
  1448  					switch job.state {
  1449  					case jobHalted:
  1450  						jobPerJobTemplate = jobsPerHaltedJobTemplate
  1451  					case jobCompleted:
  1452  						jobErrorList = job.jobHandle.Error()
  1453  						if 0 == len(jobErrorList) {
  1454  							jobPerJobTemplate = jobsPerSuccessfulJobTemplate
  1455  						} else {
  1456  							jobPerJobTemplate = jobsPerFailedJobTemplate
  1457  						}
  1458  					}
  1459  
  1460  					switch jobType {
  1461  					case fsckJobType:
  1462  						_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobPerJobTemplate, jobID, job.startTime.Format(time.RFC3339), job.endTime.Format(time.RFC3339), volumeName, "fsck")))
  1463  					case scrubJobType:
  1464  						_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobPerJobTemplate, jobID, job.startTime.Format(time.RFC3339), job.endTime.Format(time.RFC3339), volumeName, "scrub")))
  1465  					}
  1466  				}
  1467  			}
  1468  
  1469  			_, _ = responseWriter.Write([]byte(jobsListBottom))
  1470  
  1471  			if inactive {
  1472  				switch jobType {
  1473  				case fsckJobType:
  1474  					_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsStartJobButtonTemplate, volumeName, "fsck")))
  1475  				case scrubJobType:
  1476  					_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobsStartJobButtonTemplate, volumeName, "scrub")))
  1477  				}
  1478  			}
  1479  
  1480  			_, _ = responseWriter.Write([]byte(jobsBottom))
  1481  		}
  1482  
  1483  		return
  1484  	}
  1485  
  1486  	// If we reach here, numPathParts is 4
  1487  
  1488  	jobID, err = strconv.ParseUint(pathSplit[4], 10, 64)
  1489  	if nil != err {
  1490  		volume.Unlock()
  1491  		responseWriter.WriteHeader(http.StatusNotFound)
  1492  		return
  1493  	}
  1494  
  1495  	switch jobType {
  1496  	case fsckJobType:
  1497  		jobAsValue, ok, err = volume.fsckJobs.GetByKey(jobID)
  1498  	case scrubJobType:
  1499  		jobAsValue, ok, err = volume.scrubJobs.GetByKey(jobID)
  1500  	}
  1501  	if nil != err {
  1502  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1503  	}
  1504  	if !ok {
  1505  		volume.Unlock()
  1506  		responseWriter.WriteHeader(http.StatusNotFound)
  1507  		return
  1508  	}
  1509  	job = jobAsValue.(*jobStruct)
  1510  
  1511  	jobStatusJSONStruct = &JobStatusJSONPackedStruct{
  1512  		StartTime: job.startTime.Format(time.RFC3339),
  1513  		ErrorList: job.jobHandle.Error(),
  1514  		InfoList:  job.jobHandle.Info(),
  1515  	}
  1516  
  1517  	switch job.state {
  1518  	case jobRunning:
  1519  		// Nothing to add here
  1520  	case jobHalted:
  1521  		jobStatusJSONStruct.HaltTime = job.endTime.Format(time.RFC3339)
  1522  	case jobCompleted:
  1523  		jobStatusJSONStruct.DoneTime = job.endTime.Format(time.RFC3339)
  1524  	}
  1525  
  1526  	jobStatusJSONPacked, err = json.Marshal(jobStatusJSONStruct)
  1527  	if nil != err {
  1528  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1529  	}
  1530  
  1531  	if formatResponseAsJSON {
  1532  		responseWriter.Header().Set("Content-Type", "application/json")
  1533  		responseWriter.WriteHeader(http.StatusOK)
  1534  
  1535  		if formatResponseCompactly {
  1536  			_, _ = responseWriter.Write(jobStatusJSONPacked)
  1537  		} else {
  1538  			json.Indent(&jobStatusJSONBuffer, jobStatusJSONPacked, "", "\t")
  1539  			_, _ = responseWriter.Write(jobStatusJSONBuffer.Bytes())
  1540  			_, _ = responseWriter.Write([]byte("\n"))
  1541  		}
  1542  	} else {
  1543  		responseWriter.Header().Set("Content-Type", "text/html")
  1544  		responseWriter.WriteHeader(http.StatusOK)
  1545  
  1546  		switch jobType {
  1547  		case fsckJobType:
  1548  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, volumeName, "FSCK", "fsck", job.id, utils.ByteSliceToString(jobStatusJSONPacked))))
  1549  		case scrubJobType:
  1550  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(jobTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, volumeName, "SCRUB", "scrub", job.id, utils.ByteSliceToString(jobStatusJSONPacked))))
  1551  		}
  1552  	}
  1553  
  1554  	volume.Unlock()
  1555  }
  1556  
  1557  type layoutReportElementLayoutReportElementStruct struct {
  1558  	ObjectNumber uint64
  1559  	ObjectBytes  uint64
  1560  }
  1561  
  1562  type layoutReportSetElementStruct struct {
  1563  	TreeName      string
  1564  	Discrepencies uint64
  1565  	LayoutReport  []layoutReportElementLayoutReportElementStruct
  1566  }
  1567  
  1568  type layoutReportSetElementWithoutDiscrepenciesStruct struct {
  1569  	TreeName     string
  1570  	LayoutReport []layoutReportElementLayoutReportElementStruct
  1571  }
  1572  
  1573  func doLayoutReport(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1574  	var (
  1575  		discrepencyFormatClass              string
  1576  		err                                 error
  1577  		layoutReportIndex                   int
  1578  		layoutReportMap                     sortedmap.LayoutReport
  1579  		layoutReportSet                     [6]*layoutReportSetElementStruct
  1580  		layoutReportSetElement              *layoutReportSetElementStruct
  1581  		layoutReportSetJSON                 bytes.Buffer
  1582  		layoutReportSetJSONPacked           []byte
  1583  		layoutReportSetWithoutDiscrepencies [6]*layoutReportSetElementWithoutDiscrepenciesStruct
  1584  		objectBytes                         uint64
  1585  		objectNumber                        uint64
  1586  		treeTypeIndex                       int
  1587  	)
  1588  
  1589  	layoutReportSet[headhunter.MergedBPlusTree] = &layoutReportSetElementStruct{
  1590  		TreeName: "Checkpoint (Trailer + B+Trees)",
  1591  	}
  1592  	layoutReportSet[headhunter.InodeRecBPlusTree] = &layoutReportSetElementStruct{
  1593  		TreeName: "Inode Record B+Tree",
  1594  	}
  1595  	layoutReportSet[headhunter.LogSegmentRecBPlusTree] = &layoutReportSetElementStruct{
  1596  		TreeName: "Log Segment Record B+Tree",
  1597  	}
  1598  	layoutReportSet[headhunter.BPlusTreeObjectBPlusTree] = &layoutReportSetElementStruct{
  1599  		TreeName: "B+Plus Tree Objects B+Tree",
  1600  	}
  1601  	layoutReportSet[headhunter.CreatedObjectsBPlusTree] = &layoutReportSetElementStruct{
  1602  		TreeName: "Created Objects B+Tree",
  1603  	}
  1604  	layoutReportSet[headhunter.DeletedObjectsBPlusTree] = &layoutReportSetElementStruct{
  1605  		TreeName: "Deleted Objects B+Tree",
  1606  	}
  1607  
  1608  	for treeTypeIndex, layoutReportSetElement = range layoutReportSet {
  1609  		layoutReportMap, layoutReportSetElement.Discrepencies, err = requestState.volume.headhunterVolumeHandle.FetchLayoutReport(headhunter.BPlusTreeType(treeTypeIndex), requestState.performValidation)
  1610  		if nil != err {
  1611  			responseWriter.WriteHeader(http.StatusInternalServerError)
  1612  			return
  1613  		}
  1614  
  1615  		layoutReportSetElement.LayoutReport = make([]layoutReportElementLayoutReportElementStruct, len(layoutReportMap))
  1616  
  1617  		layoutReportIndex = 0
  1618  
  1619  		for objectNumber, objectBytes = range layoutReportMap {
  1620  			layoutReportSetElement.LayoutReport[layoutReportIndex] = layoutReportElementLayoutReportElementStruct{objectNumber, objectBytes}
  1621  			layoutReportIndex++
  1622  		}
  1623  	}
  1624  
  1625  	if requestState.formatResponseAsJSON {
  1626  		responseWriter.Header().Set("Content-Type", "application/json")
  1627  		responseWriter.WriteHeader(http.StatusOK)
  1628  
  1629  		if requestState.performValidation {
  1630  			layoutReportSetJSONPacked, err = json.Marshal(layoutReportSet)
  1631  		} else {
  1632  			for treeTypeIndex, layoutReportSetElement = range layoutReportSet {
  1633  				layoutReportSetWithoutDiscrepencies[treeTypeIndex] = &layoutReportSetElementWithoutDiscrepenciesStruct{
  1634  					TreeName:     layoutReportSetElement.TreeName,
  1635  					LayoutReport: layoutReportSetElement.LayoutReport,
  1636  				}
  1637  			}
  1638  			layoutReportSetJSONPacked, err = json.Marshal(layoutReportSetWithoutDiscrepencies)
  1639  		}
  1640  		if nil != err {
  1641  			responseWriter.WriteHeader(http.StatusInternalServerError)
  1642  			return
  1643  		}
  1644  
  1645  		if requestState.formatResponseCompactly {
  1646  			_, _ = responseWriter.Write(layoutReportSetJSONPacked)
  1647  		} else {
  1648  			json.Indent(&layoutReportSetJSON, layoutReportSetJSONPacked, "", "\t")
  1649  			_, _ = responseWriter.Write(layoutReportSetJSON.Bytes())
  1650  			_, _ = responseWriter.Write([]byte("\n"))
  1651  		}
  1652  	} else {
  1653  		responseWriter.Header().Set("Content-Type", "text/html")
  1654  		responseWriter.WriteHeader(http.StatusOK)
  1655  
  1656  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(layoutReportTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name)))
  1657  
  1658  		for _, layoutReportSetElement = range layoutReportSet {
  1659  			if 0 == layoutReportSetElement.Discrepencies {
  1660  				discrepencyFormatClass = "success"
  1661  			} else {
  1662  				discrepencyFormatClass = "danger"
  1663  			}
  1664  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(layoutReportTableTopTemplate, layoutReportSetElement.TreeName, layoutReportSetElement.Discrepencies, discrepencyFormatClass)))
  1665  
  1666  			for layoutReportIndex = 0; layoutReportIndex < len(layoutReportSetElement.LayoutReport); layoutReportIndex++ {
  1667  				_, _ = responseWriter.Write([]byte(fmt.Sprintf(layoutReportTableRowTemplate, layoutReportSetElement.LayoutReport[layoutReportIndex].ObjectNumber, layoutReportSetElement.LayoutReport[layoutReportIndex].ObjectBytes)))
  1668  			}
  1669  
  1670  			_, _ = responseWriter.Write([]byte(layoutReportTableBottom))
  1671  		}
  1672  
  1673  		_, _ = responseWriter.Write([]byte(layoutReportBottom))
  1674  	}
  1675  }
  1676  
  1677  func doLeaseReport(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1678  	var (
  1679  		err                   error
  1680  		leaseReport           []*jrpcfs.LeaseReportElementStruct
  1681  		leaseReportJSON       bytes.Buffer
  1682  		leaseReportJSONPacked []byte
  1683  	)
  1684  
  1685  	leaseReport, err = jrpcfs.FetchLeaseReport(requestState.volume.name)
  1686  	if nil != err {
  1687  		responseWriter.WriteHeader(http.StatusNotFound)
  1688  		return
  1689  	}
  1690  
  1691  	leaseReportJSONPacked, err = json.Marshal(leaseReport)
  1692  	if nil != err {
  1693  		responseWriter.WriteHeader(http.StatusInternalServerError)
  1694  		return
  1695  	}
  1696  
  1697  	// TODO: Need to align this logic and cleanly format page... but for now...
  1698  
  1699  	responseWriter.Header().Set("Content-Type", "application/json")
  1700  	responseWriter.WriteHeader(http.StatusOK)
  1701  
  1702  	if requestState.formatResponseCompactly {
  1703  		_, _ = responseWriter.Write(leaseReportJSONPacked)
  1704  	} else {
  1705  		json.Indent(&leaseReportJSON, leaseReportJSONPacked, "", "\t")
  1706  		_, _ = responseWriter.Write(leaseReportJSON.Bytes())
  1707  		_, _ = responseWriter.Write([]byte("\n"))
  1708  	}
  1709  }
  1710  
  1711  func doGetOfSnapShot(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  1712  	var (
  1713  		directionStringCanonicalized string
  1714  		directionStringSlice         []string
  1715  		err                          error
  1716  		list                         []headhunter.SnapShotStruct
  1717  		listJSON                     bytes.Buffer
  1718  		listJSONPacked               []byte
  1719  		orderByStringCanonicalized   string
  1720  		orderByStringSlice           []string
  1721  		queryValues                  url.Values
  1722  		reversed                     bool
  1723  		snapShot                     headhunter.SnapShotStruct
  1724  	)
  1725  
  1726  	queryValues = request.URL.Query()
  1727  
  1728  	directionStringSlice = queryValues["direction"]
  1729  
  1730  	if 0 == len(directionStringSlice) {
  1731  		reversed = false
  1732  	} else {
  1733  		directionStringCanonicalized = strings.ToLower(directionStringSlice[0])
  1734  		if "desc" == directionStringCanonicalized {
  1735  			reversed = true
  1736  		} else {
  1737  			reversed = false
  1738  		}
  1739  	}
  1740  
  1741  	orderByStringSlice = queryValues["orderby"]
  1742  
  1743  	if 0 == len(orderByStringSlice) {
  1744  		list = requestState.volume.headhunterVolumeHandle.SnapShotListByTime(reversed)
  1745  	} else {
  1746  		orderByStringCanonicalized = strings.ToLower(orderByStringSlice[0])
  1747  		switch orderByStringCanonicalized {
  1748  		case "id":
  1749  			list = requestState.volume.headhunterVolumeHandle.SnapShotListByID(reversed)
  1750  		case "name":
  1751  			list = requestState.volume.headhunterVolumeHandle.SnapShotListByName(reversed)
  1752  		default: // assume "time"
  1753  			list = requestState.volume.headhunterVolumeHandle.SnapShotListByTime(reversed)
  1754  		}
  1755  	}
  1756  
  1757  	if requestState.formatResponseAsJSON {
  1758  		responseWriter.Header().Set("Content-Type", "application/json")
  1759  		responseWriter.WriteHeader(http.StatusOK)
  1760  
  1761  		listJSONPacked, err = json.Marshal(list)
  1762  		if nil != err {
  1763  			responseWriter.WriteHeader(http.StatusInternalServerError)
  1764  			return
  1765  		}
  1766  
  1767  		if requestState.formatResponseCompactly {
  1768  			_, _ = responseWriter.Write(listJSONPacked)
  1769  		} else {
  1770  			json.Indent(&listJSON, listJSONPacked, "", "\t")
  1771  			_, _ = responseWriter.Write(listJSON.Bytes())
  1772  			_, _ = responseWriter.Write([]byte("\n"))
  1773  		}
  1774  	} else {
  1775  		responseWriter.Header().Set("Content-Type", "text/html")
  1776  		responseWriter.WriteHeader(http.StatusOK)
  1777  
  1778  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(snapShotsTopTemplate, version.ProxyFSVersion, globals.ipAddrTCPPort, requestState.volume.name)))
  1779  
  1780  		for _, snapShot = range list {
  1781  			_, _ = responseWriter.Write([]byte(fmt.Sprintf(snapShotsPerSnapShotTemplate, snapShot.ID, snapShot.Time.Format(time.RFC3339), snapShot.Name)))
  1782  		}
  1783  
  1784  		_, _ = responseWriter.Write([]byte(fmt.Sprintf(snapShotsBottomTemplate, requestState.volume.name)))
  1785  	}
  1786  }
  1787  
  1788  func doPost(responseWriter http.ResponseWriter, request *http.Request) {
  1789  	switch {
  1790  	case strings.HasPrefix(request.URL.Path, "/deletions"):
  1791  		doPostOfDeletions(responseWriter, request)
  1792  	case strings.HasPrefix(request.URL.Path, "/trigger"):
  1793  		doPostOfTrigger(responseWriter, request)
  1794  	case strings.HasPrefix(request.URL.Path, "/volume"):
  1795  		doPostOfVolume(responseWriter, request)
  1796  	default:
  1797  		responseWriter.WriteHeader(http.StatusNotFound)
  1798  	}
  1799  }
  1800  
  1801  func doPostOfDeletions(responseWriter http.ResponseWriter, request *http.Request) {
  1802  	var (
  1803  		numPathParts int
  1804  		pathSplit    []string
  1805  	)
  1806  
  1807  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
  1808  	//                                                  pathSplit[1] should be "deletions" based on how we got here
  1809  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
  1810  
  1811  	numPathParts = len(pathSplit) - 1
  1812  	if "" == pathSplit[numPathParts] {
  1813  		numPathParts--
  1814  	}
  1815  
  1816  	if "deletions" != pathSplit[1] {
  1817  		responseWriter.WriteHeader(http.StatusNotFound)
  1818  		return
  1819  	}
  1820  
  1821  	switch numPathParts {
  1822  	case 2:
  1823  		// Form: /deletions/{pause|resume}
  1824  
  1825  		switch pathSplit[2] {
  1826  		case "pause":
  1827  			headhunter.DisableObjectDeletions()
  1828  			responseWriter.WriteHeader(http.StatusNoContent)
  1829  		case "resume":
  1830  			headhunter.EnableObjectDeletions()
  1831  			responseWriter.WriteHeader(http.StatusNoContent)
  1832  		default:
  1833  			responseWriter.WriteHeader(http.StatusNotFound)
  1834  		}
  1835  	default:
  1836  		responseWriter.WriteHeader(http.StatusNotFound)
  1837  	}
  1838  }
  1839  
  1840  func doPostOfTrigger(responseWriter http.ResponseWriter, request *http.Request) {
  1841  	var (
  1842  		err                      error
  1843  		haltTriggerCountAsString string
  1844  		haltTriggerCountAsU32    uint32
  1845  		haltTriggerCountAsU64    uint64
  1846  		haltTriggerString        string
  1847  		numPathParts             int
  1848  		pathSplit                []string
  1849  	)
  1850  
  1851  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
  1852  	//                                                  pathSplit[1] should be "trigger" based on how we got here
  1853  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
  1854  
  1855  	numPathParts = len(pathSplit) - 1
  1856  	if "" == pathSplit[numPathParts] {
  1857  		numPathParts--
  1858  	}
  1859  
  1860  	if "trigger" != pathSplit[1] {
  1861  		responseWriter.WriteHeader(http.StatusNotFound)
  1862  		return
  1863  	}
  1864  
  1865  	switch numPathParts {
  1866  	case 2:
  1867  		// Form: /trigger/<trigger-name>
  1868  
  1869  		haltTriggerString = pathSplit[2]
  1870  
  1871  		_, err = halter.Stat(haltTriggerString)
  1872  		if nil != err {
  1873  			responseWriter.WriteHeader(http.StatusNotFound)
  1874  			return
  1875  		}
  1876  
  1877  		haltTriggerCountAsString = request.FormValue("count")
  1878  
  1879  		haltTriggerCountAsU64, err = strconv.ParseUint(haltTriggerCountAsString, 10, 32)
  1880  		if nil != err {
  1881  			responseWriter.WriteHeader(http.StatusBadRequest)
  1882  			return
  1883  		}
  1884  		haltTriggerCountAsU32 = uint32(haltTriggerCountAsU64)
  1885  
  1886  		if 0 == haltTriggerCountAsU32 {
  1887  			halter.Disarm(haltTriggerString)
  1888  		} else {
  1889  			halter.Arm(haltTriggerString, haltTriggerCountAsU32)
  1890  		}
  1891  
  1892  		responseWriter.WriteHeader(http.StatusNoContent)
  1893  	default:
  1894  		responseWriter.WriteHeader(http.StatusNotFound)
  1895  	}
  1896  }
  1897  
  1898  func doPostOfVolume(responseWriter http.ResponseWriter, request *http.Request) {
  1899  	var (
  1900  		acceptHeader  string
  1901  		err           error
  1902  		job           *jobStruct
  1903  		jobAsValue    sortedmap.Value
  1904  		jobID         uint64
  1905  		jobType       jobTypeType
  1906  		jobsCount     int
  1907  		numPathParts  int
  1908  		ok            bool
  1909  		pathSplit     []string
  1910  		volume        *volumeStruct
  1911  		volumeAsValue sortedmap.Value
  1912  		volumeName    string
  1913  	)
  1914  
  1915  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
  1916  	//                                                  pathSplit[1] should be "volume" based on how we got here
  1917  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
  1918  
  1919  	numPathParts = len(pathSplit) - 1
  1920  	if "" == pathSplit[numPathParts] {
  1921  		numPathParts--
  1922  	}
  1923  
  1924  	if "volume" != pathSplit[1] {
  1925  		responseWriter.WriteHeader(http.StatusNotFound)
  1926  		return
  1927  	}
  1928  
  1929  	switch numPathParts {
  1930  	case 3:
  1931  		// Form: /volume/<volume-name>/fsck-job
  1932  		// Form: /volume/<volume-name>/scrub-job
  1933  		// Form: /volume/<volume-name>/snapshot
  1934  	case 4:
  1935  		// Form: /volume/<volume-name>/fsck-job/<job-id>
  1936  		// Form: /volume/<volume-name>/scrub-job/<job-id>
  1937  	default:
  1938  		responseWriter.WriteHeader(http.StatusNotFound)
  1939  		return
  1940  	}
  1941  
  1942  	volumeName = pathSplit[2]
  1943  
  1944  	volumeAsValue, ok, err = globals.volumeLLRB.GetByKey(volumeName)
  1945  	if nil != err {
  1946  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  1947  	}
  1948  	if !ok {
  1949  		responseWriter.WriteHeader(http.StatusNotFound)
  1950  		return
  1951  	}
  1952  	volume = volumeAsValue.(*volumeStruct)
  1953  
  1954  	switch pathSplit[3] {
  1955  	case "fsck-job":
  1956  		jobType = fsckJobType
  1957  	case "scrub-job":
  1958  		jobType = scrubJobType
  1959  	case "snapshot":
  1960  		if 3 != numPathParts {
  1961  			responseWriter.WriteHeader(http.StatusNotFound)
  1962  			return
  1963  		}
  1964  		doPostOfSnapShot(responseWriter, request, volume)
  1965  		return
  1966  	default:
  1967  		responseWriter.WriteHeader(http.StatusNotFound)
  1968  		return
  1969  	}
  1970  
  1971  	volume.Lock()
  1972  
  1973  	if 3 == numPathParts {
  1974  		markJobsCompletedIfNoLongerActiveWhileLocked(volume)
  1975  
  1976  		if (nil != volume.fsckActiveJob) || (nil != volume.scrubActiveJob) {
  1977  			// Cannot start an FSCK or SCRUB job while either is active
  1978  
  1979  			volume.Unlock()
  1980  			responseWriter.WriteHeader(http.StatusPreconditionFailed)
  1981  			return
  1982  		}
  1983  
  1984  		for {
  1985  			switch jobType {
  1986  			case fsckJobType:
  1987  				jobsCount, err = volume.fsckJobs.Len()
  1988  			case scrubJobType:
  1989  				jobsCount, err = volume.scrubJobs.Len()
  1990  			}
  1991  			if nil != err {
  1992  				logger.Fatalf("HTTP Server Logic Error: %v", err)
  1993  			}
  1994  
  1995  			if jobsCount < int(globals.jobHistoryMaxSize) {
  1996  				break
  1997  			}
  1998  
  1999  			switch jobType {
  2000  			case fsckJobType:
  2001  				ok, err = volume.fsckJobs.DeleteByIndex(0)
  2002  			case scrubJobType:
  2003  				ok, err = volume.scrubJobs.DeleteByIndex(0)
  2004  			}
  2005  			if nil != err {
  2006  				logger.Fatalf("HTTP Server Logic Error: %v", err)
  2007  			}
  2008  			if !ok {
  2009  				switch jobType {
  2010  				case fsckJobType:
  2011  					err = fmt.Errorf("httpserver.doPostOfVolume() delete of oldest element of volume.fsckJobs failed")
  2012  				case scrubJobType:
  2013  					err = fmt.Errorf("httpserver.doPostOfVolume() delete of oldest element of volume.scrubJobs failed")
  2014  				}
  2015  				logger.Fatalf("HTTP Server Logic Error: %v", err)
  2016  			}
  2017  		}
  2018  
  2019  		job = &jobStruct{
  2020  			volume:    volume,
  2021  			state:     jobRunning,
  2022  			startTime: time.Now(),
  2023  		}
  2024  
  2025  		job.id = volume.headhunterVolumeHandle.FetchNonce()
  2026  
  2027  		switch jobType {
  2028  		case fsckJobType:
  2029  			ok, err = volume.fsckJobs.Put(job.id, job)
  2030  		case scrubJobType:
  2031  			ok, err = volume.scrubJobs.Put(job.id, job)
  2032  		}
  2033  		if nil != err {
  2034  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2035  		}
  2036  		if !ok {
  2037  			switch jobType {
  2038  			case fsckJobType:
  2039  				err = fmt.Errorf("httpserver.doPostOfVolume() PUT to volume.fsckJobs failed")
  2040  			case scrubJobType:
  2041  				err = fmt.Errorf("httpserver.doPostOfVolume() PUT to volume.scrubJobs failed")
  2042  			}
  2043  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2044  		}
  2045  
  2046  		switch jobType {
  2047  		case fsckJobType:
  2048  			volume.fsckActiveJob = job
  2049  
  2050  			job.jobHandle = fs.ValidateVolume(volumeName)
  2051  		case scrubJobType:
  2052  			volume.scrubActiveJob = job
  2053  
  2054  			job.jobHandle = fs.ScrubVolume(volumeName)
  2055  		}
  2056  
  2057  		volume.Unlock()
  2058  
  2059  		switch jobType {
  2060  		case fsckJobType:
  2061  			responseWriter.Header().Set("Location", fmt.Sprintf("/volume/%v/fsck-job/%v", volumeName, job.id))
  2062  		case scrubJobType:
  2063  			responseWriter.Header().Set("Location", fmt.Sprintf("/volume/%v/scrub-job/%v", volumeName, job.id))
  2064  		}
  2065  
  2066  		acceptHeader = request.Header.Get("Accept")
  2067  
  2068  		if strings.Contains(acceptHeader, "text/html") {
  2069  			responseWriter.WriteHeader(http.StatusSeeOther)
  2070  		} else {
  2071  			responseWriter.WriteHeader(http.StatusCreated)
  2072  		}
  2073  
  2074  		return
  2075  	}
  2076  
  2077  	// If we reach here, numPathParts is 4
  2078  
  2079  	jobID, err = strconv.ParseUint(pathSplit[4], 10, 64)
  2080  	if nil != err {
  2081  		volume.Unlock()
  2082  		responseWriter.WriteHeader(http.StatusNotFound)
  2083  		return
  2084  	}
  2085  
  2086  	switch jobType {
  2087  	case fsckJobType:
  2088  		jobAsValue, ok, err = volume.fsckJobs.GetByKey(jobID)
  2089  	case scrubJobType:
  2090  		jobAsValue, ok, err = volume.scrubJobs.GetByKey(jobID)
  2091  	}
  2092  	if nil != err {
  2093  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  2094  	}
  2095  	if !ok {
  2096  		volume.Unlock()
  2097  		responseWriter.WriteHeader(http.StatusNotFound)
  2098  		return
  2099  	}
  2100  	job = jobAsValue.(*jobStruct)
  2101  
  2102  	switch jobType {
  2103  	case fsckJobType:
  2104  		if volume.fsckActiveJob != job {
  2105  			volume.Unlock()
  2106  			responseWriter.WriteHeader(http.StatusPreconditionFailed)
  2107  			return
  2108  		}
  2109  	case scrubJobType:
  2110  		if volume.scrubActiveJob != job {
  2111  			volume.Unlock()
  2112  			responseWriter.WriteHeader(http.StatusPreconditionFailed)
  2113  			return
  2114  		}
  2115  	}
  2116  
  2117  	job.jobHandle.Cancel()
  2118  
  2119  	job.state = jobHalted
  2120  	job.endTime = time.Now()
  2121  
  2122  	switch jobType {
  2123  	case fsckJobType:
  2124  		volume.fsckActiveJob = nil
  2125  	case scrubJobType:
  2126  		volume.scrubActiveJob = nil
  2127  	}
  2128  
  2129  	volume.Unlock()
  2130  
  2131  	responseWriter.WriteHeader(http.StatusNoContent)
  2132  }
  2133  
  2134  func doPostOfSnapShot(responseWriter http.ResponseWriter, request *http.Request, volume *volumeStruct) {
  2135  	var (
  2136  		err        error
  2137  		snapShotID uint64
  2138  	)
  2139  
  2140  	snapShotID, err = volume.inodeVolumeHandle.SnapShotCreate(request.FormValue("name"))
  2141  	if nil == err {
  2142  		responseWriter.Header().Set("Location", fmt.Sprintf("/volume/%v/snapshot/%v", volume.name, snapShotID))
  2143  
  2144  		responseWriter.WriteHeader(http.StatusCreated)
  2145  	} else {
  2146  		responseWriter.WriteHeader(http.StatusConflict)
  2147  	}
  2148  }
  2149  
  2150  func sortedTwoColumnResponseWriter(llrb sortedmap.LLRBTree, responseWriter http.ResponseWriter) {
  2151  	var (
  2152  		err                  error
  2153  		format               string
  2154  		i                    int
  2155  		keyAsKey             sortedmap.Key
  2156  		keyAsString          string
  2157  		lenLLRB              int
  2158  		line                 string
  2159  		longestKeyAsString   int
  2160  		longestValueAsString int
  2161  		ok                   bool
  2162  		valueAsString        string
  2163  		valueAsValue         sortedmap.Value
  2164  	)
  2165  
  2166  	lenLLRB, err = llrb.Len()
  2167  	if nil != err {
  2168  		err = fmt.Errorf("llrb.Len() failed: %v", err)
  2169  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  2170  	}
  2171  
  2172  	responseWriter.Header().Set("Content-Type", "text/plain")
  2173  	responseWriter.WriteHeader(http.StatusOK)
  2174  
  2175  	longestKeyAsString = 0
  2176  	longestValueAsString = 0
  2177  
  2178  	for i = 0; i < lenLLRB; i++ {
  2179  		keyAsKey, valueAsValue, ok, err = llrb.GetByIndex(i)
  2180  		if nil != err {
  2181  			err = fmt.Errorf("llrb.GetByIndex(%v) failed: %v", i, err)
  2182  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2183  		}
  2184  		if !ok {
  2185  			err = fmt.Errorf("llrb.GetByIndex(%v) returned ok == false", i)
  2186  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2187  		}
  2188  		keyAsString = keyAsKey.(string)
  2189  		valueAsString = valueAsValue.(string)
  2190  		if len(keyAsString) > longestKeyAsString {
  2191  			longestKeyAsString = len(keyAsString)
  2192  		}
  2193  		if len(valueAsString) > longestValueAsString {
  2194  			longestValueAsString = len(valueAsString)
  2195  		}
  2196  	}
  2197  
  2198  	format = fmt.Sprintf("%%-%vs %%%vs\n", longestKeyAsString, longestValueAsString)
  2199  
  2200  	for i = 0; i < lenLLRB; i++ {
  2201  		keyAsKey, valueAsValue, ok, err = llrb.GetByIndex(i)
  2202  		if nil != err {
  2203  			err = fmt.Errorf("llrb.GetByIndex(%v) failed: %v", i, err)
  2204  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2205  		}
  2206  		if !ok {
  2207  			err = fmt.Errorf("llrb.GetByIndex(%v) returned ok == false", i)
  2208  			logger.Fatalf("HTTP Server Logic Error: %v", err)
  2209  		}
  2210  		keyAsString = keyAsKey.(string)
  2211  		valueAsString = valueAsValue.(string)
  2212  		line = fmt.Sprintf(format, keyAsString, valueAsString)
  2213  		_, _ = responseWriter.Write([]byte(line))
  2214  	}
  2215  }
  2216  
  2217  func markJobsCompletedIfNoLongerActiveWhileLocked(volume *volumeStruct) {
  2218  	// First, mark as finished now any FSCK/SCRUB job
  2219  
  2220  	if (nil != volume.fsckActiveJob) && !volume.fsckActiveJob.jobHandle.Active() {
  2221  		// FSCK job finished at some point... make it look like it just finished now
  2222  
  2223  		volume.fsckActiveJob.state = jobCompleted
  2224  		volume.fsckActiveJob.endTime = time.Now()
  2225  		volume.fsckActiveJob = nil
  2226  	}
  2227  
  2228  	if (nil != volume.scrubActiveJob) && !volume.scrubActiveJob.jobHandle.Active() {
  2229  		// SCRUB job finished at some point... make it look like it just finished now
  2230  
  2231  		volume.scrubActiveJob.state = jobCompleted
  2232  		volume.scrubActiveJob.endTime = time.Now()
  2233  		volume.scrubActiveJob = nil
  2234  	}
  2235  }
  2236  
  2237  func doPut(responseWriter http.ResponseWriter, request *http.Request) {
  2238  	switch {
  2239  	case strings.HasPrefix(request.URL.Path, "/volume"):
  2240  		doPutOfVolume(responseWriter, request)
  2241  	default:
  2242  		responseWriter.WriteHeader(http.StatusNotFound)
  2243  	}
  2244  }
  2245  
  2246  func doPutOfVolume(responseWriter http.ResponseWriter, request *http.Request) {
  2247  	var (
  2248  		err           error
  2249  		numPathParts  int
  2250  		ok            bool
  2251  		pathSplit     []string
  2252  		requestState  *requestStateStruct
  2253  		volumeAsValue sortedmap.Value
  2254  		volumeName    string
  2255  	)
  2256  
  2257  	pathSplit = strings.Split(request.URL.Path, "/") // leading  "/" places "" in pathSplit[0]
  2258  	//                                                  pathSplit[1] should be "volume" based on how we got here
  2259  	//                                                  trailing "/" places "" in pathSplit[len(pathSplit)-1]
  2260  
  2261  	numPathParts = len(pathSplit) - 1
  2262  	if "" == pathSplit[numPathParts] {
  2263  		numPathParts--
  2264  	}
  2265  
  2266  	if "volume" != pathSplit[1] {
  2267  		responseWriter.WriteHeader(http.StatusNotFound)
  2268  		return
  2269  	}
  2270  
  2271  	switch numPathParts {
  2272  	case 3:
  2273  		// Form: /volume/<volume-name>/replace-dir-entries
  2274  	default:
  2275  		responseWriter.WriteHeader(http.StatusNotFound)
  2276  		return
  2277  	}
  2278  
  2279  	volumeName = pathSplit[2]
  2280  
  2281  	volumeAsValue, ok, err = globals.volumeLLRB.GetByKey(volumeName)
  2282  	if nil != err {
  2283  		logger.Fatalf("HTTP Server Logic Error: %v", err)
  2284  	}
  2285  	if !ok {
  2286  		responseWriter.WriteHeader(http.StatusNotFound)
  2287  		return
  2288  	}
  2289  
  2290  	requestState = &requestStateStruct{
  2291  		volume: volumeAsValue.(*volumeStruct),
  2292  	}
  2293  
  2294  	switch pathSplit[3] {
  2295  	case "replace-dir-entries":
  2296  		doReplaceDirEntries(responseWriter, request, requestState)
  2297  	default:
  2298  		responseWriter.WriteHeader(http.StatusNotFound)
  2299  		return
  2300  	}
  2301  }
  2302  
  2303  // doReplaceDirEntries expects the instructions for how to replace a DirInode's directory entries
  2304  // to be passed entirely in request.Body in the following form:
  2305  //
  2306  //   (<parentDirInodeNumber>) <parentDirEntryBasename> => <dirInodeNumber>
  2307  //   <dirEntryInodeNumber A>
  2308  //   <dirEntryInodeNumber B>
  2309  //   ...
  2310  //   <dirEntryInodeNumber Z>
  2311  //
  2312  // where each InodeNumber is a 16 Hex Digit string. The list should not include "." nor "..". If
  2313  // successful, the DirInode's directory entries will be replaced by:
  2314  //
  2315  //   "."                                             => <dirInodeNumber>
  2316  //   ".."                                            => <parentDirInodeNumber>
  2317  //   <dirEntryInodeNumber A as 16 Hex Digits string> => <dirEntryInodeNumber A>
  2318  //   <dirEntryInodeNumber B as 16 Hex Digits string> => <dirEntryInodeNumber B>
  2319  //   ...
  2320  //   <dirEntryInodeNumber Z as 16 Hex Digits string> => <dirEntryInodeNumber Z>
  2321  //
  2322  // In addition, the LinkCount for the DirInode will be properly set to 2 + the number of
  2323  // dirEntryInodeNumber's that refer to subdirectories.
  2324  //
  2325  func doReplaceDirEntries(responseWriter http.ResponseWriter, request *http.Request, requestState *requestStateStruct) {
  2326  	var (
  2327  		dirEntryInodeNumber             inode.InodeNumber
  2328  		dirEntryInodeNumbers            []inode.InodeNumber
  2329  		dirInodeNumber                  inode.InodeNumber
  2330  		err                             error
  2331  		inodeNumberAsUint64             uint64
  2332  		maxNumberOfDirEntryInodeNumbers int
  2333  		parentDirEntryBasename          string
  2334  		parentDirInodeNumber            inode.InodeNumber
  2335  		requestBody                     []byte
  2336  	)
  2337  
  2338  	requestBody, err = ioutil.ReadAll(request.Body)
  2339  	if nil != err {
  2340  		_ = request.Body.Close()
  2341  		responseWriter.WriteHeader(http.StatusBadRequest)
  2342  		return
  2343  	}
  2344  
  2345  	err = request.Body.Close()
  2346  	if nil != err {
  2347  		responseWriter.WriteHeader(http.StatusBadRequest)
  2348  		return
  2349  	}
  2350  
  2351  	// Parse parentDirInodeNumber first since it is in a fixed location
  2352  
  2353  	if (24 > len(requestBody)) || ('(' != requestBody[0]) || (')' != requestBody[17]) || (' ' != requestBody[18]) {
  2354  		responseWriter.WriteHeader(http.StatusBadRequest)
  2355  		return
  2356  	}
  2357  
  2358  	inodeNumberAsUint64, err = strconv.ParseUint(string(requestBody[1:17]), 16, 64)
  2359  	if nil != err {
  2360  		responseWriter.WriteHeader(http.StatusBadRequest)
  2361  		return
  2362  	}
  2363  
  2364  	parentDirInodeNumber = inode.InodeNumber(inodeNumberAsUint64)
  2365  
  2366  	requestBody = requestBody[19:]
  2367  
  2368  	// Parse backwards reading dirEntryInodeNumbers until hitting one preceeded by " => "
  2369  
  2370  	maxNumberOfDirEntryInodeNumbers = (len(requestBody) + 15) / 16
  2371  	dirEntryInodeNumbers = make([]inode.InodeNumber, 0, maxNumberOfDirEntryInodeNumbers)
  2372  
  2373  	for {
  2374  		if 21 > len(requestBody) {
  2375  			responseWriter.WriteHeader(http.StatusBadRequest)
  2376  			return
  2377  		}
  2378  
  2379  		if " => " == string(requestBody[len(requestBody)-20:len(requestBody)-16]) {
  2380  			break
  2381  		}
  2382  
  2383  		inodeNumberAsUint64, err = strconv.ParseUint(string(requestBody[len(requestBody)-16:]), 16, 64)
  2384  		if nil != err {
  2385  			responseWriter.WriteHeader(http.StatusBadRequest)
  2386  			return
  2387  		}
  2388  
  2389  		dirEntryInodeNumber = inode.InodeNumber(inodeNumberAsUint64)
  2390  		dirEntryInodeNumbers = append(dirEntryInodeNumbers, dirEntryInodeNumber)
  2391  
  2392  		requestBody = requestBody[:len(requestBody)-16]
  2393  	}
  2394  
  2395  	// Parse dirInodeNumber
  2396  
  2397  	inodeNumberAsUint64, err = strconv.ParseUint(string(requestBody[len(requestBody)-16:]), 16, 64)
  2398  	if nil != err {
  2399  		responseWriter.WriteHeader(http.StatusBadRequest)
  2400  		return
  2401  	}
  2402  
  2403  	dirInodeNumber = inode.InodeNumber(inodeNumberAsUint64)
  2404  
  2405  	requestBody = requestBody[:len(requestBody)-20]
  2406  
  2407  	// What remains is parentDirEntryBasename
  2408  
  2409  	parentDirEntryBasename = string(requestBody[:])
  2410  
  2411  	// Now we can finally proceed
  2412  
  2413  	err = requestState.volume.inodeVolumeHandle.ReplaceDirEntries(parentDirInodeNumber, parentDirEntryBasename, dirInodeNumber, dirEntryInodeNumbers)
  2414  	if nil == err {
  2415  		responseWriter.WriteHeader(http.StatusCreated)
  2416  	} else {
  2417  		responseWriter.WriteHeader(http.StatusBadRequest)
  2418  	}
  2419  }