github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/git-lob-serve/serverstore.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/atlassian/git-lob/core"
    12  	"github.com/atlassian/git-lob/providers/smart"
    13  	"github.com/atlassian/git-lob/util"
    14  )
    15  
    16  // A server could choose to store LOBs however it likes
    17  // For simplicity, this server chooses to store the LOB files in the same structure as the client does,
    18  // with the addition that it also stores cached binary deltas.
    19  
    20  // We re-use a bunch of the client code here for storage and utility functions but it's important to realise
    21  // that a server implementation doesn't have to adhere to the same rules as the client, it only has to
    22  // implement the smart protocol. The re-use here is simply to avoid code duplication given that we're storing
    23  // in the same structure, and is not a requirement for any alternative server implementations.
    24  
    25  // Get the absolute path to the root directory containing LOB files for the config & path
    26  // Does not create the directory nor validate that config is correct
    27  func getLOBRoot(config *Config, path string) string {
    28  	return filepath.Join(config.BasePath, path)
    29  }
    30  
    31  // Get the absolute path of a LOB chunk file
    32  // Does not create the directory nor validate that config is correct
    33  func getLOBChunkFilePath(sha string, chunk int, config *Config, path string) string {
    34  	return filepath.Join(getLOBRoot(config, path), core.GetLOBChunkRelativePath(sha, chunk))
    35  }
    36  
    37  // Get the absolute path of a LOB meta file
    38  // Does not create the directory nor validate that config is correct
    39  func getLOBMetaFilePath(sha string, config *Config, path string) string {
    40  	return filepath.Join(getLOBRoot(config, path), core.GetLOBMetaRelativePath(sha))
    41  }
    42  
    43  // Generic method to get file path based on type (meta/chunk)
    44  // Does not create the directory nor validate that config is correct
    45  func getLOBFilePath(sha, filetype string, chunk int, config *Config, path string) string {
    46  	if filetype == "chunk" {
    47  		return getLOBChunkFilePath(sha, chunk, config, path)
    48  	} else if filetype == "meta" {
    49  		return getLOBMetaFilePath(sha, config, path)
    50  	}
    51  	// error
    52  	return ""
    53  }
    54  
    55  // Gets the path to a file which contains delta from one sha to another
    56  func getLOBDeltaFilePath(basesha, targetsha string, config *Config, path string) string {
    57  	return filepath.Join(config.DeltaCachePath, fmt.Sprintf("%v_%v", basesha, targetsha))
    58  }
    59  
    60  func fileExists(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
    61  	freq := smart.FileExistsRequest{}
    62  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &freq)
    63  	if err != nil {
    64  		return smart.NewJsonErrorResponse(req.Id, err.Error())
    65  	}
    66  	result := smart.FileExistsResponse{}
    67  	file := getLOBFilePath(freq.LobSHA, freq.Type, freq.ChunkIdx, config, path)
    68  	if file == "" {
    69  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unsupported file type: %v", freq.Type))
    70  	}
    71  	s, err := os.Stat(file)
    72  	if err == nil {
    73  		result.Exists = true
    74  		result.Size = s.Size()
    75  	} // otherwise defaults false/0
    76  
    77  	resp, err := smart.NewJsonResponse(req.Id, result)
    78  	if err != nil {
    79  		return smart.NewJsonErrorResponse(req.Id, err.Error())
    80  	}
    81  	return resp
    82  }
    83  
    84  func fileExistsOfSize(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
    85  	freq := smart.FileExistsOfSizeRequest{}
    86  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &freq)
    87  	if err != nil {
    88  		return smart.NewJsonErrorResponse(req.Id, err.Error())
    89  	}
    90  	result := smart.FileExistsOfSizeResponse{}
    91  	file := getLOBFilePath(freq.LobSHA, freq.Type, freq.ChunkIdx, config, path)
    92  	if file == "" {
    93  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unsupported file type: %v", freq.Type))
    94  	}
    95  
    96  	result.Result = util.FileExistsAndIsOfSize(file, freq.Size)
    97  
    98  	resp, err := smart.NewJsonResponse(req.Id, result)
    99  	if err != nil {
   100  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   101  	}
   102  	return resp
   103  }
   104  
   105  func ensureDirExists(dir string, cfg *Config) error {
   106  	if !util.DirExists(dir) {
   107  		// Get permissions from base path & match (or default to user/group write)
   108  		mode := os.FileMode(0775)
   109  		s, err := os.Stat(cfg.BasePath)
   110  		if err == nil {
   111  			mode = s.Mode()
   112  		}
   113  		return os.MkdirAll(dir, mode)
   114  	}
   115  	return nil
   116  }
   117  
   118  const transferBufferSize = int64(128 * 1024)
   119  
   120  func uploadFile(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   121  	upreq := smart.UploadFileRequest{}
   122  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &upreq)
   123  	if err != nil {
   124  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   125  	}
   126  	startresult := smart.UploadFileStartResponse{}
   127  	startresult.OKToSend = true
   128  	// Send start response immediately
   129  	resp, err := smart.NewJsonResponse(req.Id, startresult)
   130  	if err != nil {
   131  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   132  	}
   133  	err = sendResponse(resp, out)
   134  	if err != nil {
   135  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   136  	}
   137  	// Next from client should be byte stream of exactly the stated number of bytes
   138  	// Write to temporary file then move to final on success
   139  	file := getLOBFilePath(upreq.LobSHA, upreq.Type, upreq.ChunkIdx, config, path)
   140  	if file == "" {
   141  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unsupported file type: %v", upreq.Type))
   142  	}
   143  
   144  	// Now open temp file to write to
   145  	outf, err := ioutil.TempFile("", "tempchunk")
   146  	defer outf.Close()
   147  	n, err := io.CopyN(outf, in, upreq.Size)
   148  	if err != nil {
   149  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unable to read data: %v", err.Error()))
   150  	} else if n != upreq.Size {
   151  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Received wrong number of bytes %d (expected %d)", n, upreq.Size))
   152  	}
   153  
   154  	receivedresult := smart.UploadFileCompleteResponse{}
   155  	receivedresult.ReceivedOK = true
   156  	var receiveerr string
   157  	// force close now before defer so we can copy
   158  	err = outf.Close()
   159  	if err != nil {
   160  		receivedresult.ReceivedOK = false
   161  		receiveerr = fmt.Sprintf("Error when closing temp file: %v", err.Error())
   162  	} else {
   163  		// ensure final directory exists
   164  		ensureDirExists(filepath.Dir(file), config)
   165  		// Move temp file to final location
   166  		err = os.Rename(outf.Name(), file)
   167  		if err != nil {
   168  			receivedresult.ReceivedOK = false
   169  			receiveerr = fmt.Sprintf("Error when closing temp file: %v", err.Error())
   170  		}
   171  
   172  	}
   173  
   174  	resp, _ = smart.NewJsonResponse(req.Id, receivedresult)
   175  	if receiveerr != "" {
   176  		resp.Error = receiveerr
   177  	}
   178  
   179  	return resp
   180  
   181  }
   182  
   183  func downloadFilePrepare(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   184  	downreq := smart.DownloadFilePrepareRequest{}
   185  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &downreq)
   186  	if err != nil {
   187  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   188  	}
   189  	file := getLOBFilePath(downreq.LobSHA, downreq.Type, downreq.ChunkIdx, config, path)
   190  	if file == "" {
   191  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unsupported file type: %v", downreq.Type))
   192  	}
   193  	result := smart.DownloadFilePrepareResponse{}
   194  	s, err := os.Stat(file)
   195  	if err != nil {
   196  		// file doesn't exist, this should not have been called
   197  		return smart.NewJsonErrorResponse(req.Id, "File doesn't exist")
   198  	}
   199  	result.Size = s.Size()
   200  	resp, err := smart.NewJsonResponse(req.Id, result)
   201  	if err != nil {
   202  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   203  	}
   204  	return resp
   205  
   206  }
   207  
   208  func downloadFileStart(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   209  	downreq := smart.DownloadFileStartRequest{}
   210  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &downreq)
   211  	if err != nil {
   212  		// Serve() copes with converting this to stderr rather than JSON response
   213  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   214  	}
   215  	file := getLOBFilePath(downreq.LobSHA, downreq.Type, downreq.ChunkIdx, config, path)
   216  	if file == "" {
   217  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unsupported file type: %v", downreq.Type))
   218  	}
   219  	// check size
   220  	s, err := os.Stat(file)
   221  	if err != nil {
   222  		// file doesn't exist, this should not have been called
   223  		return smart.NewJsonErrorResponse(req.Id, "File doesn't exist")
   224  	}
   225  	if s.Size() != downreq.Size {
   226  		// This won't work!
   227  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("File sizes disagree (client: %d server: %d)", downreq.Size, s.Size()))
   228  	}
   229  
   230  	f, err := os.OpenFile(file, os.O_RDONLY, 0644)
   231  	if err != nil {
   232  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   233  	}
   234  	defer f.Close()
   235  
   236  	n, err := io.Copy(out, f)
   237  	if err != nil {
   238  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error copying data to output: %v", err.Error()))
   239  	}
   240  	if n != s.Size() {
   241  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Amount of data copied disagrees (expected: %d actual: %d)", s.Size(), n))
   242  	}
   243  	if err != nil {
   244  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error copying data to output: %v", err.Error()))
   245  	}
   246  	if n != s.Size() {
   247  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Amount of data copied disagrees (expected: %d actual: %d)", s.Size(), n))
   248  	}
   249  
   250  	// Don't return a response, only response is byte stream above except in error cases
   251  	return nil
   252  }
   253  
   254  func pickCompleteLOB(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   255  	params := smart.GetFirstCompleteLOBFromListRequest{}
   256  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &params)
   257  	if err != nil {
   258  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   259  	}
   260  	result := smart.GetFirstCompleteLOBFromListResponse{}
   261  	for _, candidatesha := range params.LobSHAs {
   262  		// We need to stop on the first valid & complete SHA
   263  		// Only checking presence & size here, not checking hash
   264  		if core.CheckLOBFilesForSHA(candidatesha, getLOBRoot(config, path), false) == nil {
   265  			result.FirstSHA = candidatesha
   266  			break
   267  		}
   268  
   269  	}
   270  	// If we didn't find any, result.FirstSHA = "" which is correct per protocol
   271  	resp, err := smart.NewJsonResponse(req.Id, result)
   272  	if err != nil {
   273  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   274  	}
   275  	return resp
   276  }
   277  
   278  func lobExists(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   279  	params := smart.LOBExistsRequest{}
   280  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &params)
   281  	if err != nil {
   282  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   283  	}
   284  	result := smart.LOBExistsResponse{}
   285  	_, sz, err := core.GetLOBFilesForSHA(params.LobSHA, getLOBRoot(config, path), true, false)
   286  	// in the case of error, assume missing so return default false
   287  	if err == nil {
   288  		result.Exists = true
   289  		result.Size = sz
   290  	}
   291  	resp, err := smart.NewJsonResponse(req.Id, result)
   292  	if err != nil {
   293  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   294  	}
   295  	return resp
   296  }
   297  
   298  func uploadDelta(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   299  	upreq := smart.UploadDeltaRequest{}
   300  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &upreq)
   301  	if err != nil {
   302  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   303  	}
   304  	startresult := smart.UploadDeltaStartResponse{}
   305  	startresult.OKToSend = true
   306  	if upreq.Size > config.DeltaSizeLimit {
   307  		// reject this, cause client to fall back
   308  		startresult.OKToSend = false
   309  		resp, err := smart.NewJsonResponse(req.Id, startresult)
   310  		if err != nil {
   311  			return smart.NewJsonErrorResponse(req.Id, err.Error())
   312  		}
   313  		return resp
   314  	}
   315  
   316  	// Otherwise continue
   317  	// Send start response immediately
   318  	resp, err := smart.NewJsonResponse(req.Id, startresult)
   319  	if err != nil {
   320  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   321  	}
   322  	err = sendResponse(resp, out)
   323  	if err != nil {
   324  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   325  	}
   326  	// Next from client should be byte stream of exactly the stated number of bytes
   327  	// Write to temporary file then move to final on success
   328  	outf, err := ioutil.TempFile("", "tempchunk")
   329  	if err != nil {
   330  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error when opening temp file: %v", err.Error()))
   331  	}
   332  	// If any errors, delete the temp file automatically (will fail silently if already moved)
   333  	defer os.Remove(outf.Name())
   334  	defer outf.Close()
   335  	n, err := io.CopyN(outf, in, upreq.Size)
   336  	if err != nil {
   337  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Unable to read data: %v", err.Error()))
   338  	} else if n != upreq.Size {
   339  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Received wrong number of bytes %d (expected %d)", n, upreq.Size))
   340  	}
   341  
   342  	receivedresult := smart.UploadDeltaCompleteResponse{}
   343  	receivedresult.ReceivedOK = true
   344  	// force close now before defer so we can copy, if this works
   345  	tempdeltafilename := outf.Name()
   346  	err = outf.Close()
   347  
   348  	if err != nil {
   349  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error when closing temp file: %v", err.Error()))
   350  	}
   351  
   352  	// Apply the patch from the temp file, to make sure it applies ok
   353  	// Other servers might choose just to store the delta and to not store the applied result, but we will
   354  	// we sacrifice some data storage for saved CPU work later
   355  	indeltaf, err := os.OpenFile(tempdeltafilename, os.O_RDONLY, 0644)
   356  	if err != nil {
   357  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error re-opening delta file for apply: %v", err.Error()))
   358  	}
   359  	defer indeltaf.Close()
   360  	lobroot := getLOBRoot(config, path)
   361  	ensureDirExists(lobroot, config)
   362  	err = core.ApplyLOBDeltaInBaseDir(lobroot, upreq.BaseLobSHA, upreq.TargetLobSHA, indeltaf)
   363  	if err != nil {
   364  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error when applying delta: %v", err.Error()))
   365  	}
   366  
   367  	// Now save the delta so we can use it later on in DownloadDelta for other clients
   368  	// Ignore any errors on renaming, just means it won't be in the cache (inconvenient but not fatal, temp will be deleted on return)
   369  	file := getLOBDeltaFilePath(upreq.BaseLobSHA, upreq.TargetLobSHA, config, path)
   370  	if file != "" {
   371  		// ensure final directory exists
   372  		ensureDirExists(filepath.Dir(file), config)
   373  		// Move temp file to final location
   374  		// We keep all deltas, we can use them to send to clients too (saves calculating)
   375  		// Should have a cron which deletes old ones
   376  		os.Rename(outf.Name(), file)
   377  	}
   378  
   379  	resp, err = smart.NewJsonResponse(req.Id, receivedresult)
   380  	if err != nil {
   381  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   382  	}
   383  	return resp
   384  
   385  }
   386  
   387  func downloadDeltaPrepare(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   388  	downreq := smart.DownloadDeltaPrepareRequest{}
   389  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &downreq)
   390  	if err != nil {
   391  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   392  	}
   393  	result := smart.DownloadDeltaPrepareResponse{}
   394  	// First see if we have this delta in the cache already
   395  	deltafile := getLOBDeltaFilePath(downreq.BaseLobSHA, downreq.TargetLobSHA, config, path)
   396  	s, err := os.Stat(deltafile)
   397  	if err == nil {
   398  		result.Size = s.Size()
   399  	} else {
   400  		// either there was no cache file or we need to regen
   401  		lobroot := getLOBRoot(config, path)
   402  		var deltabuf bytes.Buffer
   403  		sz, err := core.GenerateLOBDeltaInBaseDir(lobroot, downreq.BaseLobSHA, downreq.TargetLobSHA, &deltabuf)
   404  		if err != nil {
   405  			return smart.NewJsonErrorResponse(req.Id, err.Error())
   406  		}
   407  		result.Size = sz
   408  
   409  		// Write this delta to cache, via temp + rename to ensure not interrupted
   410  		tempf, err := ioutil.TempFile("", "deltatemp")
   411  		if err == nil {
   412  			defer os.Remove(tempf.Name()) // in case any errors
   413  			n, err := tempf.Write(deltabuf.Bytes())
   414  			tempf.Close()
   415  			if err == nil && n == deltabuf.Len() {
   416  				// only rename to final if correct size & no errors (don't want to bake incorrect delta
   417  				// don't check error here, if it doesn't work we just don't store in cache (and defer deletes))
   418  				os.Rename(tempf.Name(), deltafile)
   419  			}
   420  		}
   421  	}
   422  
   423  	resp, err := smart.NewJsonResponse(req.Id, result)
   424  	if err != nil {
   425  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   426  	}
   427  	return resp
   428  }
   429  func downloadDeltaStart(req *smart.JsonRequest, in io.Reader, out io.Writer, config *Config, path string) *smart.JsonResponse {
   430  	downreq := smart.DownloadDeltaStartRequest{}
   431  	err := smart.ExtractStructFromJsonRawMessage(req.Params, &downreq)
   432  	if err != nil {
   433  		return smart.NewJsonErrorResponse(req.Id, err.Error())
   434  	}
   435  	deltafile := getLOBDeltaFilePath(downreq.BaseLobSHA, downreq.TargetLobSHA, config, path)
   436  	if !util.FileExistsAndIsOfSize(deltafile, downreq.Size) {
   437  		// Caller will turn this into stderr output
   438  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Delta file for %v/%v is not present or is wrong size (not %d), cannot send. Did you call 'prepare'?",
   439  			downreq.BaseLobSHA, downreq.TargetLobSHA, downreq.Size))
   440  	}
   441  
   442  	deltaf, err := os.OpenFile(deltafile, os.O_RDONLY, 0644)
   443  	if err != nil {
   444  		return smart.NewJsonErrorResponse(req.Id, err)
   445  	}
   446  	n, err := io.Copy(out, deltaf)
   447  	if err != nil {
   448  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Error copying delta data to output: %v", err.Error()))
   449  	}
   450  	if n != downreq.Size {
   451  		return smart.NewJsonErrorResponse(req.Id, fmt.Sprintf("Amount of delta data copied disagrees (expected: %d actual: %d)", downreq.Size, n))
   452  	}
   453  
   454  	// There is no response, just data above
   455  	return nil
   456  }