github.com/decred/politeia@v1.4.0/politeiad/cmd/legacypoliteia/git_filepath.go (about) 1 // Copyright (c) 2022 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "fmt" 9 "io/fs" 10 "regexp" 11 "sort" 12 "strconv" 13 14 "path/filepath" 15 16 "github.com/decred/politeia/politeiad/cmd/legacypoliteia/gitbe" 17 ) 18 19 // git_filepath.go contains the code that parses information from the legacy 20 // git repo file paths and that determines the file paths of the various pieces 21 // of proposal data. 22 23 // parseProposalTokens recursively walks the provided directory and returns an 24 // inventory of all legacy proposal tokens that are parsed from the git repo 25 // file paths. The tokens are returned in alphabetical order. 26 func parseProposalTokens(dirPath string) ([]string, error) { 27 tokens := make(map[string]struct{}, 256) 28 err := filepath.WalkDir(dirPath, 29 func(path string, d fs.DirEntry, err error) error { 30 // We only care about directories 31 if !d.IsDir() { 32 return nil 33 } 34 35 // Parse the token from the path 36 token, ok := parseProposalToken(path) 37 if ok { 38 tokens[token] = struct{}{} 39 return nil 40 } 41 42 return nil 43 }) 44 if err != nil { 45 return nil, err 46 } 47 48 // Put the tokens into a slice and sort them alphabetically 49 sorted := make([]string, 0, len(tokens)) 50 for token := range tokens { 51 sorted = append(sorted, token) 52 } 53 sort.SliceStable(sorted, func(i, j int) bool { 54 return sorted[i] < sorted[j] 55 }) 56 57 return sorted, nil 58 } 59 60 var ( 61 // Regular expression that matches the git proposal token from a proposal 62 // parent directory. 63 expLegacyProposalToken = `[0-9a-f]{64}` 64 regexLegacyProposalToken = regexp.MustCompile(expLegacyProposalToken) 65 ) 66 67 // parseProposalToken parses and returns the legacy proposal token from a 68 // git repo path. 69 // 70 // Input: 71 // "fdd68c87961549750adf29e178128210cb310294080211cf6a35792aa1bb7f63.json" 72 // Output: 73 // "fdd68c87961549750adf29e178128210cb310294080211cf6a35792aa1bb7f63", true 74 // 75 // Input: 76 // "mainnet/fdd68c87961549750adf29e178128210cb310294080211cf6a35792aa1bb7f63/1" 77 // Output: 78 // "fdd68c87961549750adf29e178128210cb310294080211cf6a35792aa1bb7f63", true 79 func parseProposalToken(path string) (string, bool) { 80 var ( 81 token = regexLegacyProposalToken.FindString(path) 82 ok = (token != "") 83 ) 84 return token, ok 85 } 86 87 // praseLatestProposalVersion parses latest version of a legacy proposal from 88 // the git file path and returns it. 89 // 90 // Example path: [gitRepo]/[token]/[version]/ 91 func parseLatestProposalVersion(gitRepo, token string) (uint64, error) { 92 // Compile a list of all directories. The version numbers 93 // are the directory name. 94 dirs := make(map[string]struct{}, 64) 95 err := filepath.WalkDir(filepath.Join(gitRepo, token), 96 func(path string, d fs.DirEntry, err error) error { 97 if !d.IsDir() { 98 return nil 99 } 100 dirs[d.Name()] = struct{}{} 101 return nil 102 }) 103 if err != nil { 104 return 0, err 105 } 106 107 // Parse the version number from the directory name 108 versions := make(map[uint64]struct{}, 64) 109 for dirname := range dirs { 110 u, err := strconv.ParseUint(dirname, 10, 32) 111 if err != nil { 112 // Not a version directory 113 continue 114 } 115 versions[u] = struct{}{} 116 } 117 if err != nil { 118 return 0, err 119 } 120 121 // Find the most recent version 122 var latest uint64 123 for version := range versions { 124 if version > latest { 125 latest = version 126 } 127 } 128 if latest == 0 { 129 return 0, fmt.Errorf("latest version not found for %v", token) 130 } 131 132 return latest, nil 133 } 134 135 var ( 136 // Regular expersion that matches the token and version number from a 137 // proposal directory path. 138 expProposalVersion = `[0-9a-f]{64}\/[0-9]{1,}` 139 regexProposalVersion = regexp.MustCompile(expProposalVersion) 140 ) 141 142 // proposalVersion parses the version number for the proposal directory path 143 // and returns it. 144 func parseProposalVersion(proposalDir string) (uint32, error) { 145 var ( 146 subPath = regexProposalVersion.FindString(proposalDir) 147 versionStr = filepath.Base(subPath) 148 ) 149 u, err := strconv.ParseUint(versionStr, 10, 32) 150 if err != nil { 151 return 0, err 152 } 153 154 return uint32(u), nil 155 } 156 157 // parseProposalAttachmentFiles parses the filenames of all proposal attachment 158 // files and returns them. This function does NOT return the file path, just 159 // the file name. The proposal index file and proposal metadata file are not 160 // considered to be attachments. 161 func parseProposalAttachmentFilenames(proposalDir string) ([]string, error) { 162 var ( 163 notAnAttachmentFile = map[string]struct{}{ 164 gitbe.IndexFilename: {}, 165 gitbe.ProposalMetadataFilename: {}, 166 } 167 168 payloadDir = payloadDirPath(proposalDir) 169 filenames = make([]string, 0, 64) 170 ) 171 172 // Walk the payload directory 173 err := filepath.WalkDir(payloadDir, 174 func(path string, d fs.DirEntry, err error) error { 175 // There shouldn't be any nested directories 176 // in the payload directory, but check just 177 // in case. 178 if d.IsDir() { 179 return nil 180 } 181 182 if _, ok := notAnAttachmentFile[d.Name()]; ok { 183 // Not an attachment; skip 184 return nil 185 } 186 187 // This is an attachment file 188 filenames = append(filenames, d.Name()) 189 190 return nil 191 }) 192 if err != nil { 193 return nil, err 194 } 195 196 return filenames, nil 197 } 198 199 func recordMetadataPath(proposalDir string) string { 200 return filepath.Join(proposalDir, gitbe.RecordMetadataFilename) 201 } 202 203 func payloadDirPath(proposalDir string) string { 204 return filepath.Join(proposalDir, gitbe.RecordPayloadPath) 205 } 206 207 func indexFilePath(proposalDir string) string { 208 return filepath.Join(payloadDirPath(proposalDir), gitbe.IndexFilename) 209 } 210 211 func attachmentFilePath(proposalDir, attachmentFilename string) string { 212 return filepath.Join(payloadDirPath(proposalDir), attachmentFilename) 213 } 214 215 func proposalMetadataPath(proposalDir string) string { 216 return filepath.Join(payloadDirPath(proposalDir), 217 gitbe.ProposalMetadataFilename) 218 } 219 220 func proposalGeneralPath(proposalDir string) string { 221 return filepath.Join(proposalDir, gitbe.MDStreamProposalGeneral) 222 } 223 224 func statusChangesPath(proposalDir string) string { 225 return filepath.Join(proposalDir, gitbe.MDStreamStatusChanges) 226 } 227 228 func commentsJournalPath(proposalDir string) string { 229 return filepath.Join(decredPluginPath(proposalDir), 230 gitbe.CommentsJournalFilename) 231 } 232 233 func authorizeVotePath(proposalDir string) string { 234 return filepath.Join(proposalDir, gitbe.MDStreamAuthorizeVote) 235 } 236 237 func startVotePath(proposalDir string) string { 238 return filepath.Join(proposalDir, gitbe.MDStreamStartVote) 239 } 240 241 func startVoteReplyPath(proposalDir string) string { 242 return filepath.Join(proposalDir, gitbe.MDStreamStartVoteReply) 243 } 244 245 func decredPluginPath(proposalDir string) string { 246 return filepath.Join(proposalDir, gitbe.DecredPluginPath) 247 } 248 249 func ballotsJournalPath(proposalDir string) string { 250 return filepath.Join(decredPluginPath(proposalDir), 251 gitbe.BallotJournalFilename) 252 }