jottacloud.go 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162
  1. // Package jottacloud provides an interface to the Jottacloud storage system.
  2. package jottacloud
  3. import (
  4. "bytes"
  5. "context"
  6. "crypto/md5"
  7. "encoding/base64"
  8. "encoding/hex"
  9. "encoding/json"
  10. "encoding/xml"
  11. "errors"
  12. "fmt"
  13. "io"
  14. "math/rand"
  15. "net/http"
  16. "net/url"
  17. "os"
  18. "path"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "github.com/rclone/rclone/backend/jottacloud/api"
  23. "github.com/rclone/rclone/fs"
  24. "github.com/rclone/rclone/fs/accounting"
  25. "github.com/rclone/rclone/fs/config"
  26. "github.com/rclone/rclone/fs/config/configmap"
  27. "github.com/rclone/rclone/fs/config/configstruct"
  28. "github.com/rclone/rclone/fs/config/obscure"
  29. "github.com/rclone/rclone/fs/fserrors"
  30. "github.com/rclone/rclone/fs/fshttp"
  31. "github.com/rclone/rclone/fs/hash"
  32. "github.com/rclone/rclone/fs/walk"
  33. "github.com/rclone/rclone/lib/encoder"
  34. "github.com/rclone/rclone/lib/oauthutil"
  35. "github.com/rclone/rclone/lib/pacer"
  36. "github.com/rclone/rclone/lib/rest"
  37. "golang.org/x/oauth2"
  38. )
  39. // Globals
  40. const (
  41. minSleep = 10 * time.Millisecond
  42. maxSleep = 2 * time.Second
  43. decayConstant = 2 // bigger for slower decay, exponential
  44. defaultDevice = "Jotta"
  45. defaultMountpoint = "Archive"
  46. jfsURL = "https://jfs.jottacloud.com/jfs/"
  47. apiURL = "https://api.jottacloud.com/"
  48. wwwURL = "https://www.jottacloud.com/"
  49. cachePrefix = "rclone-jcmd5-"
  50. configDevice = "device"
  51. configMountpoint = "mountpoint"
  52. configTokenURL = "tokenURL"
  53. configClientID = "client_id"
  54. configClientSecret = "client_secret"
  55. configUsername = "username"
  56. configVersion = 1
  57. defaultTokenURL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/token"
  58. defaultClientID = "jottacli"
  59. legacyTokenURL = "https://api.jottacloud.com/auth/v1/token"
  60. legacyRegisterURL = "https://api.jottacloud.com/auth/v1/register"
  61. legacyClientID = "nibfk8biu12ju7hpqomr8b1e40"
  62. legacyEncryptedClientSecret = "Vp8eAv7eVElMnQwN-kgU9cbhgApNDaMqWdlDi5qFydlQoji4JBxrGMF2"
  63. legacyConfigVersion = 0
  64. teliaseCloudTokenURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/token"
  65. teliaseCloudAuthURL = "https://cloud-auth.telia.se/auth/realms/telia_se/protocol/openid-connect/auth"
  66. teliaseCloudClientID = "desktop"
  67. telianoCloudTokenURL = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/token"
  68. telianoCloudAuthURL = "https://sky-auth.telia.no/auth/realms/get/protocol/openid-connect/auth"
  69. telianoCloudClientID = "desktop"
  70. tele2CloudTokenURL = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/token"
  71. tele2CloudAuthURL = "https://mittcloud-auth.tele2.se/auth/realms/comhem/protocol/openid-connect/auth"
  72. tele2CloudClientID = "desktop"
  73. onlimeCloudTokenURL = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/token"
  74. onlimeCloudAuthURL = "https://cloud-auth.onlime.dk/auth/realms/onlime_wl/protocol/openid-connect/auth"
  75. onlimeCloudClientID = "desktop"
  76. )
  77. // Register with Fs
  78. func init() {
  79. // needs to be done early so we can use oauth during config
  80. fs.Register(&fs.RegInfo{
  81. Name: "jottacloud",
  82. Description: "Jottacloud",
  83. NewFs: NewFs,
  84. Config: Config,
  85. MetadataInfo: &fs.MetadataInfo{
  86. Help: `Jottacloud has limited support for metadata, currently an extended set of timestamps.`,
  87. System: map[string]fs.MetadataHelp{
  88. "btime": {
  89. Help: "Time of file birth (creation), read from rclone metadata",
  90. Type: "RFC 3339",
  91. Example: "2006-01-02T15:04:05.999999999Z07:00",
  92. },
  93. "mtime": {
  94. Help: "Time of last modification, read from rclone metadata",
  95. Type: "RFC 3339",
  96. Example: "2006-01-02T15:04:05.999999999Z07:00",
  97. },
  98. "utime": {
  99. Help: "Time of last upload, when current revision was created, generated by backend",
  100. Type: "RFC 3339",
  101. Example: "2006-01-02T15:04:05.999999999Z07:00",
  102. ReadOnly: true,
  103. },
  104. "content-type": {
  105. Help: "MIME type, also known as media type",
  106. Type: "string",
  107. Example: "text/plain",
  108. ReadOnly: true,
  109. },
  110. },
  111. },
  112. Options: append(oauthutil.SharedOptions, []fs.Option{{
  113. Name: "md5_memory_limit",
  114. Help: "Files bigger than this will be cached on disk to calculate the MD5 if required.",
  115. Default: fs.SizeSuffix(10 * 1024 * 1024),
  116. Advanced: true,
  117. }, {
  118. Name: "trashed_only",
  119. Help: "Only show files that are in the trash.\n\nThis will show trashed files in their original directory structure.",
  120. Default: false,
  121. Advanced: true,
  122. }, {
  123. Name: "hard_delete",
  124. Help: "Delete files permanently rather than putting them into the trash.",
  125. Default: false,
  126. Advanced: true,
  127. }, {
  128. Name: "upload_resume_limit",
  129. Help: "Files bigger than this can be resumed if the upload fail's.",
  130. Default: fs.SizeSuffix(10 * 1024 * 1024),
  131. Advanced: true,
  132. }, {
  133. Name: "no_versions",
  134. Help: "Avoid server side versioning by deleting files and recreating files instead of overwriting them.",
  135. Default: false,
  136. Advanced: true,
  137. }, {
  138. Name: config.ConfigEncoding,
  139. Help: config.ConfigEncodingHelp,
  140. Advanced: true,
  141. // Encode invalid UTF-8 bytes as xml doesn't handle them properly.
  142. //
  143. // Also: '*', '/', ':', '<', '>', '?', '\"', '\x00', '|'
  144. Default: (encoder.Display |
  145. encoder.EncodeWin | // :?"*<>|
  146. encoder.EncodeInvalidUtf8),
  147. }}...),
  148. })
  149. }
  150. // Config runs the backend configuration protocol
  151. func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
  152. switch config.State {
  153. case "":
  154. return fs.ConfigChooseExclusiveFixed("auth_type_done", "config_type", `Select authentication type.`, []fs.OptionExample{{
  155. Value: "standard",
  156. Help: "Standard authentication.\nUse this if you're a normal Jottacloud user.",
  157. }, {
  158. Value: "legacy",
  159. Help: "Legacy authentication.\nThis is only required for certain whitelabel versions of Jottacloud and not recommended for normal users.",
  160. }, {
  161. Value: "telia_se",
  162. Help: "Telia Cloud authentication.\nUse this if you are using Telia Cloud (Sweden).",
  163. }, {
  164. Value: "telia_no",
  165. Help: "Telia Sky authentication.\nUse this if you are using Telia Sky (Norway).",
  166. }, {
  167. Value: "tele2",
  168. Help: "Tele2 Cloud authentication.\nUse this if you are using Tele2 Cloud.",
  169. }, {
  170. Value: "onlime",
  171. Help: "Onlime Cloud authentication.\nUse this if you are using Onlime Cloud.",
  172. }})
  173. case "auth_type_done":
  174. // Jump to next state according to config chosen
  175. return fs.ConfigGoto(config.Result)
  176. case "standard": // configure a jottacloud backend using the modern JottaCli token based authentication
  177. m.Set("configVersion", fmt.Sprint(configVersion))
  178. return fs.ConfigInput("standard_token", "config_login_token", "Personal login token.\nGenerate here: https://www.jottacloud.com/web/secure")
  179. case "standard_token":
  180. loginToken := config.Result
  181. m.Set(configClientID, defaultClientID)
  182. m.Set(configClientSecret, "")
  183. srv := rest.NewClient(fshttp.NewClient(ctx))
  184. token, tokenEndpoint, err := doTokenAuth(ctx, srv, loginToken)
  185. if err != nil {
  186. return nil, fmt.Errorf("failed to get oauth token: %w", err)
  187. }
  188. m.Set(configTokenURL, tokenEndpoint)
  189. err = oauthutil.PutToken(name, m, &token, true)
  190. if err != nil {
  191. return nil, fmt.Errorf("error while saving token: %w", err)
  192. }
  193. return fs.ConfigGoto("choose_device")
  194. case "legacy": // configure a jottacloud backend using legacy authentication
  195. m.Set("configVersion", fmt.Sprint(legacyConfigVersion))
  196. return fs.ConfigConfirm("legacy_api", false, "config_machine_specific", `Do you want to create a machine specific API key?
  197. Rclone has it's own Jottacloud API KEY which works fine as long as one
  198. only uses rclone on a single machine. When you want to use rclone with
  199. this account on more than one machine it's recommended to create a
  200. machine specific API key. These keys can NOT be shared between
  201. machines.`)
  202. case "legacy_api":
  203. srv := rest.NewClient(fshttp.NewClient(ctx))
  204. if config.Result == "true" {
  205. deviceRegistration, err := registerDevice(ctx, srv)
  206. if err != nil {
  207. return nil, fmt.Errorf("failed to register device: %w", err)
  208. }
  209. m.Set(configClientID, deviceRegistration.ClientID)
  210. m.Set(configClientSecret, obscure.MustObscure(deviceRegistration.ClientSecret))
  211. fs.Debugf(nil, "Got clientID %q and clientSecret %q", deviceRegistration.ClientID, deviceRegistration.ClientSecret)
  212. }
  213. return fs.ConfigInput("legacy_username", "config_username", "Username (e-mail address)")
  214. case "legacy_username":
  215. m.Set(configUsername, config.Result)
  216. return fs.ConfigPassword("legacy_password", "config_password", "Password (only used in setup, will not be stored)")
  217. case "legacy_password":
  218. m.Set("password", config.Result)
  219. m.Set("auth_code", "")
  220. return fs.ConfigGoto("legacy_do_auth")
  221. case "legacy_auth_code":
  222. authCode := strings.ReplaceAll(config.Result, "-", "") // remove any "-" contained in the code so we have a 6 digit number
  223. m.Set("auth_code", authCode)
  224. return fs.ConfigGoto("legacy_do_auth")
  225. case "legacy_do_auth":
  226. username, _ := m.Get(configUsername)
  227. password, _ := m.Get("password")
  228. password = obscure.MustReveal(password)
  229. authCode, _ := m.Get("auth_code")
  230. srv := rest.NewClient(fshttp.NewClient(ctx))
  231. clientID, ok := m.Get(configClientID)
  232. if !ok {
  233. clientID = legacyClientID
  234. }
  235. clientSecret, ok := m.Get(configClientSecret)
  236. if !ok {
  237. clientSecret = legacyEncryptedClientSecret
  238. }
  239. oauthConfig := &oauth2.Config{
  240. Endpoint: oauth2.Endpoint{
  241. AuthURL: legacyTokenURL,
  242. },
  243. ClientID: clientID,
  244. ClientSecret: obscure.MustReveal(clientSecret),
  245. }
  246. token, err := doLegacyAuth(ctx, srv, oauthConfig, username, password, authCode)
  247. if err == errAuthCodeRequired {
  248. return fs.ConfigInput("legacy_auth_code", "config_auth_code", "Verification Code\nThis account uses 2 factor authentication you will receive a verification code via SMS.")
  249. }
  250. m.Set("password", "")
  251. m.Set("auth_code", "")
  252. if err != nil {
  253. return nil, fmt.Errorf("failed to get oauth token: %w", err)
  254. }
  255. err = oauthutil.PutToken(name, m, &token, true)
  256. if err != nil {
  257. return nil, fmt.Errorf("error while saving token: %w", err)
  258. }
  259. return fs.ConfigGoto("choose_device")
  260. case "telia_se": // telia_se cloud config
  261. m.Set("configVersion", fmt.Sprint(configVersion))
  262. m.Set(configClientID, teliaseCloudClientID)
  263. m.Set(configTokenURL, teliaseCloudTokenURL)
  264. return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
  265. OAuth2Config: &oauth2.Config{
  266. Endpoint: oauth2.Endpoint{
  267. AuthURL: teliaseCloudAuthURL,
  268. TokenURL: teliaseCloudTokenURL,
  269. },
  270. ClientID: teliaseCloudClientID,
  271. Scopes: []string{"openid", "jotta-default", "offline_access"},
  272. RedirectURL: oauthutil.RedirectLocalhostURL,
  273. },
  274. })
  275. case "telia_no": // telia_no cloud config
  276. m.Set("configVersion", fmt.Sprint(configVersion))
  277. m.Set(configClientID, telianoCloudClientID)
  278. m.Set(configTokenURL, telianoCloudTokenURL)
  279. return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
  280. OAuth2Config: &oauth2.Config{
  281. Endpoint: oauth2.Endpoint{
  282. AuthURL: telianoCloudAuthURL,
  283. TokenURL: telianoCloudTokenURL,
  284. },
  285. ClientID: telianoCloudClientID,
  286. Scopes: []string{"openid", "jotta-default", "offline_access"},
  287. RedirectURL: oauthutil.RedirectLocalhostURL,
  288. },
  289. })
  290. case "tele2": // tele2 cloud config
  291. m.Set("configVersion", fmt.Sprint(configVersion))
  292. m.Set(configClientID, tele2CloudClientID)
  293. m.Set(configTokenURL, tele2CloudTokenURL)
  294. return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
  295. OAuth2Config: &oauth2.Config{
  296. Endpoint: oauth2.Endpoint{
  297. AuthURL: tele2CloudAuthURL,
  298. TokenURL: tele2CloudTokenURL,
  299. },
  300. ClientID: tele2CloudClientID,
  301. Scopes: []string{"openid", "jotta-default", "offline_access"},
  302. RedirectURL: oauthutil.RedirectLocalhostURL,
  303. },
  304. })
  305. case "onlime": // onlime cloud config
  306. m.Set("configVersion", fmt.Sprint(configVersion))
  307. m.Set(configClientID, onlimeCloudClientID)
  308. m.Set(configTokenURL, onlimeCloudTokenURL)
  309. return oauthutil.ConfigOut("choose_device", &oauthutil.Options{
  310. OAuth2Config: &oauth2.Config{
  311. Endpoint: oauth2.Endpoint{
  312. AuthURL: onlimeCloudAuthURL,
  313. TokenURL: onlimeCloudTokenURL,
  314. },
  315. ClientID: onlimeCloudClientID,
  316. Scopes: []string{"openid", "jotta-default", "offline_access"},
  317. RedirectURL: oauthutil.RedirectLocalhostURL,
  318. },
  319. })
  320. case "choose_device":
  321. return fs.ConfigConfirm("choose_device_query", false, "config_non_standard", `Use a non-standard device/mountpoint?
  322. Choosing no, the default, will let you access the storage used for the archive
  323. section of the official Jottacloud client. If you instead want to access the
  324. sync or the backup section, for example, you must choose yes.`)
  325. case "choose_device_query":
  326. if config.Result != "true" {
  327. m.Set(configDevice, "")
  328. m.Set(configMountpoint, "")
  329. return fs.ConfigGoto("end")
  330. }
  331. oAuthClient, _, err := getOAuthClient(ctx, name, m)
  332. if err != nil {
  333. return nil, err
  334. }
  335. jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
  336. apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
  337. cust, err := getCustomerInfo(ctx, apiSrv)
  338. if err != nil {
  339. return nil, err
  340. }
  341. acc, err := getDriveInfo(ctx, jfsSrv, cust.Username)
  342. if err != nil {
  343. return nil, err
  344. }
  345. deviceNames := make([]string, len(acc.Devices))
  346. for i, dev := range acc.Devices {
  347. if i > 0 && dev.Name == defaultDevice {
  348. // Insert the special Jotta device as first entry, making it the default choice.
  349. copy(deviceNames[1:i+1], deviceNames[0:i])
  350. deviceNames[0] = dev.Name
  351. } else {
  352. deviceNames[i] = dev.Name
  353. }
  354. }
  355. help := fmt.Sprintf(`The device to use. In standard setup the built-in %s device is used,
  356. which contains predefined mountpoints for archive, sync etc. All other devices
  357. are treated as backup devices by the official Jottacloud client. You may create
  358. a new by entering a unique name.`, defaultDevice)
  359. return fs.ConfigChoose("choose_device_result", "config_device", help, len(deviceNames), func(i int) (string, string) {
  360. return deviceNames[i], ""
  361. })
  362. case "choose_device_result":
  363. device := config.Result
  364. oAuthClient, _, err := getOAuthClient(ctx, name, m)
  365. if err != nil {
  366. return nil, err
  367. }
  368. jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
  369. apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
  370. cust, err := getCustomerInfo(ctx, apiSrv)
  371. if err != nil {
  372. return nil, err
  373. }
  374. acc, err := getDriveInfo(ctx, jfsSrv, cust.Username)
  375. if err != nil {
  376. return nil, err
  377. }
  378. isNew := true
  379. for _, dev := range acc.Devices {
  380. if strings.EqualFold(dev.Name, device) { // If device name exists with different casing we prefer the existing (not sure if and how the api handles the opposite)
  381. device = dev.Name // Prefer same casing as existing, e.g. if user entered "jotta" we use the standard casing "Jotta" instead
  382. isNew = false
  383. break
  384. }
  385. }
  386. var dev *api.JottaDevice
  387. if isNew {
  388. fs.Debugf(nil, "Creating new device: %s", device)
  389. dev, err = createDevice(ctx, jfsSrv, path.Join(cust.Username, device))
  390. if err != nil {
  391. return nil, err
  392. }
  393. }
  394. m.Set(configDevice, device)
  395. if !isNew {
  396. dev, err = getDeviceInfo(ctx, jfsSrv, path.Join(cust.Username, device))
  397. if err != nil {
  398. return nil, err
  399. }
  400. }
  401. var help string
  402. if device == defaultDevice {
  403. // With built-in Jotta device the mountpoint choice is exclusive,
  404. // we do not want to risk any problems by creating new mountpoints on it.
  405. help = fmt.Sprintf(`The mountpoint to use on the built-in device %s.
  406. The standard setup is to use the %s mountpoint. Most other mountpoints
  407. have very limited support in rclone and should generally be avoided.`, defaultDevice, defaultMountpoint)
  408. return fs.ConfigChooseExclusive("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) {
  409. return dev.MountPoints[i].Name, ""
  410. })
  411. }
  412. help = fmt.Sprintf(`The mountpoint to use on the non-standard device %s.
  413. You may create a new by entering a unique name.`, device)
  414. return fs.ConfigChoose("choose_device_mountpoint", "config_mountpoint", help, len(dev.MountPoints), func(i int) (string, string) {
  415. return dev.MountPoints[i].Name, ""
  416. })
  417. case "choose_device_mountpoint":
  418. mountpoint := config.Result
  419. oAuthClient, _, err := getOAuthClient(ctx, name, m)
  420. if err != nil {
  421. return nil, err
  422. }
  423. jfsSrv := rest.NewClient(oAuthClient).SetRoot(jfsURL)
  424. apiSrv := rest.NewClient(oAuthClient).SetRoot(apiURL)
  425. cust, err := getCustomerInfo(ctx, apiSrv)
  426. if err != nil {
  427. return nil, err
  428. }
  429. device, _ := m.Get(configDevice)
  430. dev, err := getDeviceInfo(ctx, jfsSrv, path.Join(cust.Username, device))
  431. if err != nil {
  432. return nil, err
  433. }
  434. isNew := true
  435. for _, mnt := range dev.MountPoints {
  436. if strings.EqualFold(mnt.Name, mountpoint) {
  437. mountpoint = mnt.Name
  438. isNew = false
  439. break
  440. }
  441. }
  442. if isNew {
  443. if device == defaultDevice {
  444. return nil, fmt.Errorf("custom mountpoints not supported on built-in %s device: %w", defaultDevice, err)
  445. }
  446. fs.Debugf(nil, "Creating new mountpoint: %s", mountpoint)
  447. _, err := createMountPoint(ctx, jfsSrv, path.Join(cust.Username, device, mountpoint))
  448. if err != nil {
  449. return nil, err
  450. }
  451. }
  452. m.Set(configMountpoint, mountpoint)
  453. return fs.ConfigGoto("end")
  454. case "end":
  455. // All the config flows end up here in case we need to carry on with something
  456. return nil, nil
  457. }
  458. return nil, fmt.Errorf("unknown state %q", config.State)
  459. }
  460. // Options defines the configuration for this backend
  461. type Options struct {
  462. Device string `config:"device"`
  463. Mountpoint string `config:"mountpoint"`
  464. MD5MemoryThreshold fs.SizeSuffix `config:"md5_memory_limit"`
  465. TrashedOnly bool `config:"trashed_only"`
  466. HardDelete bool `config:"hard_delete"`
  467. NoVersions bool `config:"no_versions"`
  468. UploadThreshold fs.SizeSuffix `config:"upload_resume_limit"`
  469. Enc encoder.MultiEncoder `config:"encoding"`
  470. }
  471. // Fs represents a remote jottacloud
  472. type Fs struct {
  473. name string
  474. root string
  475. user string
  476. opt Options
  477. features *fs.Features
  478. fileEndpoint string
  479. allocateEndpoint string
  480. jfsSrv *rest.Client
  481. apiSrv *rest.Client
  482. pacer *fs.Pacer
  483. tokenRenewer *oauthutil.Renew // renew the token on expiry
  484. }
  485. // Object describes a jottacloud object
  486. //
  487. // Will definitely have info but maybe not meta
  488. type Object struct {
  489. fs *Fs
  490. remote string
  491. hasMetaData bool
  492. size int64
  493. createTime time.Time
  494. modTime time.Time
  495. updateTime time.Time
  496. md5 string
  497. mimeType string
  498. }
  499. // ------------------------------------------------------------
  500. // Name of the remote (as passed into NewFs)
  501. func (f *Fs) Name() string {
  502. return f.name
  503. }
  504. // Root of the remote (as passed into NewFs)
  505. func (f *Fs) Root() string {
  506. return f.root
  507. }
  508. // String converts this Fs to a string
  509. func (f *Fs) String() string {
  510. return fmt.Sprintf("jottacloud root '%s'", f.root)
  511. }
  512. // Features returns the optional features of this Fs
  513. func (f *Fs) Features() *fs.Features {
  514. return f.features
  515. }
  516. // joinPath joins two path/url elements
  517. //
  518. // Does not perform clean on the result like path.Join does,
  519. // which breaks urls by changing prefix "https://" into "https:/".
  520. func joinPath(base string, rel string) string {
  521. if rel == "" {
  522. return base
  523. }
  524. if strings.HasSuffix(base, "/") {
  525. return base + strings.TrimPrefix(rel, "/")
  526. }
  527. if strings.HasPrefix(rel, "/") {
  528. return strings.TrimSuffix(base, "/") + rel
  529. }
  530. return base + "/" + rel
  531. }
  532. // retryErrorCodes is a slice of error codes that we will retry
  533. var retryErrorCodes = []int{
  534. 429, // Too Many Requests.
  535. 500, // Internal Server Error
  536. 502, // Bad Gateway
  537. 503, // Service Unavailable
  538. 504, // Gateway Timeout
  539. 509, // Bandwidth Limit Exceeded
  540. }
  541. // shouldRetry returns a boolean as to whether this resp and err
  542. // deserve to be retried. It returns the err as a convenience
  543. func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
  544. if fserrors.ContextError(ctx, &err) {
  545. return false, err
  546. }
  547. return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
  548. }
  549. // registerDevice register a new device for use with the jottacloud API
  550. func registerDevice(ctx context.Context, srv *rest.Client) (reg *api.DeviceRegistrationResponse, err error) {
  551. // random generator to generate random device names
  552. seededRand := rand.New(rand.NewSource(time.Now().UnixNano()))
  553. randonDeviceNamePartLength := 21
  554. randomDeviceNamePart := make([]byte, randonDeviceNamePartLength)
  555. charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  556. for i := range randomDeviceNamePart {
  557. randomDeviceNamePart[i] = charset[seededRand.Intn(len(charset))]
  558. }
  559. randomDeviceName := "rclone-" + string(randomDeviceNamePart)
  560. fs.Debugf(nil, "Trying to register device '%s'", randomDeviceName)
  561. values := url.Values{}
  562. values.Set("device_id", randomDeviceName)
  563. opts := rest.Opts{
  564. Method: "POST",
  565. RootURL: legacyRegisterURL,
  566. ContentType: "application/x-www-form-urlencoded",
  567. ExtraHeaders: map[string]string{"Authorization": "Bearer c2xrZmpoYWRsZmFramhkc2xma2phaHNkbGZramhhc2xkZmtqaGFzZGxrZmpobGtq"},
  568. Parameters: values,
  569. }
  570. var deviceRegistration *api.DeviceRegistrationResponse
  571. _, err = srv.CallJSON(ctx, &opts, nil, &deviceRegistration)
  572. return deviceRegistration, err
  573. }
  574. var errAuthCodeRequired = errors.New("auth code required")
  575. // doLegacyAuth runs the actual token request for V1 authentication
  576. //
  577. // Call this first with blank authCode. If errAuthCodeRequired is
  578. // returned then call it again with an authCode
  579. func doLegacyAuth(ctx context.Context, srv *rest.Client, oauthConfig *oauth2.Config, username, password, authCode string) (token oauth2.Token, err error) {
  580. // prepare out token request with username and password
  581. values := url.Values{}
  582. values.Set("grant_type", "PASSWORD")
  583. values.Set("password", password)
  584. values.Set("username", username)
  585. values.Set("client_id", oauthConfig.ClientID)
  586. values.Set("client_secret", oauthConfig.ClientSecret)
  587. opts := rest.Opts{
  588. Method: "POST",
  589. RootURL: oauthConfig.Endpoint.AuthURL,
  590. ContentType: "application/x-www-form-urlencoded",
  591. Parameters: values,
  592. }
  593. if authCode != "" {
  594. opts.ExtraHeaders = make(map[string]string)
  595. opts.ExtraHeaders["X-Jottacloud-Otp"] = authCode
  596. }
  597. // do the first request
  598. var jsonToken api.TokenJSON
  599. resp, err := srv.CallJSON(ctx, &opts, nil, &jsonToken)
  600. if err != nil && authCode == "" {
  601. // if 2fa is enabled the first request is expected to fail. We will do another request with the 2fa code as an additional http header
  602. if resp != nil {
  603. if resp.Header.Get("X-JottaCloud-OTP") == "required; SMS" {
  604. return token, errAuthCodeRequired
  605. }
  606. }
  607. }
  608. token.AccessToken = jsonToken.AccessToken
  609. token.RefreshToken = jsonToken.RefreshToken
  610. token.TokenType = jsonToken.TokenType
  611. token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second)
  612. return token, err
  613. }
  614. // doTokenAuth runs the actual token request for V2 authentication
  615. func doTokenAuth(ctx context.Context, apiSrv *rest.Client, loginTokenBase64 string) (token oauth2.Token, tokenEndpoint string, err error) {
  616. loginTokenBytes, err := base64.RawURLEncoding.DecodeString(loginTokenBase64)
  617. if err != nil {
  618. return token, "", err
  619. }
  620. // decode login token
  621. var loginToken api.LoginToken
  622. decoder := json.NewDecoder(bytes.NewReader(loginTokenBytes))
  623. err = decoder.Decode(&loginToken)
  624. if err != nil {
  625. return token, "", err
  626. }
  627. // retrieve endpoint urls
  628. opts := rest.Opts{
  629. Method: "GET",
  630. RootURL: loginToken.WellKnownLink,
  631. }
  632. var wellKnown api.WellKnown
  633. _, err = apiSrv.CallJSON(ctx, &opts, nil, &wellKnown)
  634. if err != nil {
  635. return token, "", err
  636. }
  637. // prepare out token request with username and password
  638. values := url.Values{}
  639. values.Set("client_id", defaultClientID)
  640. values.Set("grant_type", "password")
  641. values.Set("password", loginToken.AuthToken)
  642. values.Set("scope", "openid offline_access")
  643. values.Set("username", loginToken.Username)
  644. values.Encode()
  645. opts = rest.Opts{
  646. Method: "POST",
  647. RootURL: wellKnown.TokenEndpoint,
  648. ContentType: "application/x-www-form-urlencoded",
  649. Body: strings.NewReader(values.Encode()),
  650. }
  651. // do the first request
  652. var jsonToken api.TokenJSON
  653. _, err = apiSrv.CallJSON(ctx, &opts, nil, &jsonToken)
  654. if err != nil {
  655. return token, "", err
  656. }
  657. token.AccessToken = jsonToken.AccessToken
  658. token.RefreshToken = jsonToken.RefreshToken
  659. token.TokenType = jsonToken.TokenType
  660. token.Expiry = time.Now().Add(time.Duration(jsonToken.ExpiresIn) * time.Second)
  661. return token, wellKnown.TokenEndpoint, err
  662. }
  663. // getCustomerInfo queries general information about the account
  664. func getCustomerInfo(ctx context.Context, apiSrv *rest.Client) (info *api.CustomerInfo, err error) {
  665. opts := rest.Opts{
  666. Method: "GET",
  667. Path: "account/v1/customer",
  668. }
  669. _, err = apiSrv.CallJSON(ctx, &opts, nil, &info)
  670. if err != nil {
  671. return nil, fmt.Errorf("couldn't get customer info: %w", err)
  672. }
  673. return info, nil
  674. }
  675. // getDriveInfo queries general information about the account and the available devices and mountpoints.
  676. func getDriveInfo(ctx context.Context, srv *rest.Client, username string) (info *api.DriveInfo, err error) {
  677. opts := rest.Opts{
  678. Method: "GET",
  679. Path: username,
  680. }
  681. _, err = srv.CallXML(ctx, &opts, nil, &info)
  682. if err != nil {
  683. return nil, fmt.Errorf("couldn't get drive info: %w", err)
  684. }
  685. return info, nil
  686. }
  687. // getDeviceInfo queries Information about a jottacloud device
  688. func getDeviceInfo(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) {
  689. opts := rest.Opts{
  690. Method: "GET",
  691. Path: urlPathEscape(path),
  692. }
  693. _, err = srv.CallXML(ctx, &opts, nil, &info)
  694. if err != nil {
  695. return nil, fmt.Errorf("couldn't get device info: %w", err)
  696. }
  697. return info, nil
  698. }
  699. // createDevice makes a device
  700. func createDevice(ctx context.Context, srv *rest.Client, path string) (info *api.JottaDevice, err error) {
  701. opts := rest.Opts{
  702. Method: "POST",
  703. Path: urlPathEscape(path),
  704. Parameters: url.Values{},
  705. }
  706. opts.Parameters.Set("type", "WORKSTATION")
  707. _, err = srv.CallXML(ctx, &opts, nil, &info)
  708. if err != nil {
  709. return nil, fmt.Errorf("couldn't create device: %w", err)
  710. }
  711. return info, nil
  712. }
  713. // createMountPoint makes a mount point
  714. func createMountPoint(ctx context.Context, srv *rest.Client, path string) (info *api.JottaMountPoint, err error) {
  715. opts := rest.Opts{
  716. Method: "POST",
  717. Path: urlPathEscape(path),
  718. }
  719. _, err = srv.CallXML(ctx, &opts, nil, &info)
  720. if err != nil {
  721. return nil, fmt.Errorf("couldn't create mountpoint: %w", err)
  722. }
  723. return info, nil
  724. }
  725. // setEndpoints generates the API endpoints
  726. func (f *Fs) setEndpoints() {
  727. if f.opt.Device == "" {
  728. f.opt.Device = defaultDevice
  729. }
  730. if f.opt.Mountpoint == "" {
  731. f.opt.Mountpoint = defaultMountpoint
  732. }
  733. f.fileEndpoint = path.Join(f.user, f.opt.Device, f.opt.Mountpoint)
  734. f.allocateEndpoint = path.Join("/jfs", f.opt.Device, f.opt.Mountpoint)
  735. }
  736. // readMetaDataForPath reads the metadata from the path
  737. func (f *Fs) readMetaDataForPath(ctx context.Context, path string) (info *api.JottaFile, err error) {
  738. opts := rest.Opts{
  739. Method: "GET",
  740. Path: f.filePath(path),
  741. }
  742. var result api.JottaFile
  743. var resp *http.Response
  744. err = f.pacer.Call(func() (bool, error) {
  745. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
  746. return shouldRetry(ctx, resp, err)
  747. })
  748. if apiErr, ok := err.(*api.Error); ok {
  749. // does not exist
  750. if apiErr.StatusCode == http.StatusNotFound {
  751. return nil, fs.ErrorObjectNotFound
  752. }
  753. }
  754. if err != nil {
  755. return nil, fmt.Errorf("read metadata failed: %w", err)
  756. }
  757. if result.XMLName.Local == "folder" {
  758. return nil, fs.ErrorIsDir
  759. } else if result.XMLName.Local != "file" {
  760. return nil, fs.ErrorNotAFile
  761. }
  762. return &result, nil
  763. }
  764. // errorHandler parses a non 2xx error response into an error
  765. func errorHandler(resp *http.Response) error {
  766. // Decode error response
  767. errResponse := new(api.Error)
  768. err := rest.DecodeXML(resp, &errResponse)
  769. if err != nil {
  770. fs.Debugf(nil, "Couldn't decode error response: %v", err)
  771. }
  772. if errResponse.Message == "" {
  773. errResponse.Message = resp.Status
  774. }
  775. if errResponse.StatusCode == 0 {
  776. errResponse.StatusCode = resp.StatusCode
  777. }
  778. return errResponse
  779. }
  780. // Jottacloud wants '+' to be URL encoded even though the RFC states it's not reserved
  781. func urlPathEscape(in string) string {
  782. return strings.ReplaceAll(rest.URLPathEscape(in), "+", "%2B")
  783. }
  784. // filePathRaw returns an unescaped file path (f.root, file)
  785. // Optionally made absolute by prefixing with "/", typically required when used
  786. // as request parameter instead of the path (which is relative to some root url).
  787. func (f *Fs) filePathRaw(file string, absolute bool) string {
  788. prefix := ""
  789. if absolute {
  790. prefix = "/"
  791. }
  792. return path.Join(prefix, f.fileEndpoint, f.opt.Enc.FromStandardPath(path.Join(f.root, file)))
  793. }
  794. // filePath returns an escaped file path (f.root, file)
  795. func (f *Fs) filePath(file string) string {
  796. return urlPathEscape(f.filePathRaw(file, false))
  797. }
  798. // allocatePathRaw returns an unescaped allocate file path (f.root, file)
  799. // Optionally made absolute by prefixing with "/", typically required when used
  800. // as request parameter instead of the path (which is relative to some root url).
  801. func (f *Fs) allocatePathRaw(file string, absolute bool) string {
  802. prefix := ""
  803. if absolute {
  804. prefix = "/"
  805. }
  806. return path.Join(prefix, f.allocateEndpoint, f.opt.Enc.FromStandardPath(path.Join(f.root, file)))
  807. }
  808. // Jottacloud requires the grant_type 'refresh_token' string
  809. // to be uppercase and throws a 400 Bad Request if we use the
  810. // lower case used by the oauth2 module
  811. //
  812. // This filter catches all refresh requests, reads the body,
  813. // changes the case and then sends it on
  814. func grantTypeFilter(req *http.Request) {
  815. if legacyTokenURL == req.URL.String() {
  816. // read the entire body
  817. refreshBody, err := io.ReadAll(req.Body)
  818. if err != nil {
  819. return
  820. }
  821. _ = req.Body.Close()
  822. // make the refresh token upper case
  823. refreshBody = []byte(strings.Replace(string(refreshBody), "grant_type=refresh_token", "grant_type=REFRESH_TOKEN", 1))
  824. // set the new ReadCloser (with a dummy Close())
  825. req.Body = io.NopCloser(bytes.NewReader(refreshBody))
  826. }
  827. }
  828. func getOAuthClient(ctx context.Context, name string, m configmap.Mapper) (oAuthClient *http.Client, ts *oauthutil.TokenSource, err error) {
  829. // Check config version
  830. var ver int
  831. version, ok := m.Get("configVersion")
  832. if ok {
  833. ver, err = strconv.Atoi(version)
  834. if err != nil {
  835. return nil, nil, errors.New("failed to parse config version")
  836. }
  837. ok = (ver == configVersion) || (ver == legacyConfigVersion)
  838. }
  839. if !ok {
  840. return nil, nil, errors.New("outdated config - please reconfigure this backend")
  841. }
  842. baseClient := fshttp.NewClient(ctx)
  843. oauthConfig := &oauth2.Config{
  844. Endpoint: oauth2.Endpoint{
  845. AuthURL: defaultTokenURL,
  846. TokenURL: defaultTokenURL,
  847. },
  848. }
  849. if ver == configVersion {
  850. oauthConfig.ClientID = defaultClientID
  851. // if custom endpoints are set use them else stick with defaults
  852. if tokenURL, ok := m.Get(configTokenURL); ok {
  853. oauthConfig.Endpoint.TokenURL = tokenURL
  854. // jottacloud is weird. we need to use the tokenURL as authURL
  855. oauthConfig.Endpoint.AuthURL = tokenURL
  856. }
  857. } else if ver == legacyConfigVersion {
  858. clientID, ok := m.Get(configClientID)
  859. if !ok {
  860. clientID = legacyClientID
  861. }
  862. clientSecret, ok := m.Get(configClientSecret)
  863. if !ok {
  864. clientSecret = legacyEncryptedClientSecret
  865. }
  866. oauthConfig.ClientID = clientID
  867. oauthConfig.ClientSecret = obscure.MustReveal(clientSecret)
  868. oauthConfig.Endpoint.TokenURL = legacyTokenURL
  869. oauthConfig.Endpoint.AuthURL = legacyTokenURL
  870. // add the request filter to fix token refresh
  871. if do, ok := baseClient.Transport.(interface {
  872. SetRequestFilter(f func(req *http.Request))
  873. }); ok {
  874. do.SetRequestFilter(grantTypeFilter)
  875. } else {
  876. fs.Debugf(name+":", "Couldn't add request filter - uploads will fail")
  877. }
  878. }
  879. // Create OAuth Client
  880. oAuthClient, ts, err = oauthutil.NewClientWithBaseClient(ctx, name, m, oauthConfig, baseClient)
  881. if err != nil {
  882. return nil, nil, fmt.Errorf("failed to configure Jottacloud oauth client: %w", err)
  883. }
  884. return oAuthClient, ts, nil
  885. }
  886. // NewFs constructs an Fs from the path, container:path
  887. func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
  888. // Parse config into Options struct
  889. opt := new(Options)
  890. err := configstruct.Set(m, opt)
  891. if err != nil {
  892. return nil, err
  893. }
  894. oAuthClient, ts, err := getOAuthClient(ctx, name, m)
  895. if err != nil {
  896. return nil, err
  897. }
  898. rootIsDir := strings.HasSuffix(root, "/")
  899. root = strings.Trim(root, "/")
  900. f := &Fs{
  901. name: name,
  902. root: root,
  903. opt: *opt,
  904. jfsSrv: rest.NewClient(oAuthClient).SetRoot(jfsURL),
  905. apiSrv: rest.NewClient(oAuthClient).SetRoot(apiURL),
  906. pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
  907. }
  908. f.features = (&fs.Features{
  909. CaseInsensitive: true,
  910. CanHaveEmptyDirectories: true,
  911. ReadMimeType: true,
  912. WriteMimeType: false,
  913. ReadMetadata: true,
  914. WriteMetadata: true,
  915. UserMetadata: false,
  916. }).Fill(ctx, f)
  917. f.jfsSrv.SetErrorHandler(errorHandler)
  918. if opt.TrashedOnly { // we cannot support showing Trashed Files when using ListR right now
  919. f.features.ListR = nil
  920. }
  921. // Renew the token in the background
  922. f.tokenRenewer = oauthutil.NewRenew(f.String(), ts, func() error {
  923. _, err := f.readMetaDataForPath(ctx, "")
  924. if err == fs.ErrorNotAFile || err == fs.ErrorIsDir {
  925. err = nil
  926. }
  927. return err
  928. })
  929. cust, err := getCustomerInfo(ctx, f.apiSrv)
  930. if err != nil {
  931. return nil, err
  932. }
  933. f.user = cust.Username
  934. f.setEndpoints()
  935. if root != "" && !rootIsDir {
  936. // Check to see if the root actually an existing file
  937. remote := path.Base(root)
  938. f.root = path.Dir(root)
  939. if f.root == "." {
  940. f.root = ""
  941. }
  942. _, err := f.NewObject(context.TODO(), remote)
  943. if err != nil {
  944. if errors.Is(err, fs.ErrorObjectNotFound) || errors.Is(err, fs.ErrorNotAFile) || errors.Is(err, fs.ErrorIsDir) {
  945. // File doesn't exist so return old f
  946. f.root = root
  947. return f, nil
  948. }
  949. return nil, err
  950. }
  951. // return an error with an fs which points to the parent
  952. return f, fs.ErrorIsFile
  953. }
  954. return f, nil
  955. }
  956. // Return an Object from a path
  957. //
  958. // If it can't be found it returns the error fs.ErrorObjectNotFound.
  959. func (f *Fs) newObjectWithInfo(ctx context.Context, remote string, info *api.JottaFile) (fs.Object, error) {
  960. o := &Object{
  961. fs: f,
  962. remote: remote,
  963. }
  964. var err error
  965. if info != nil {
  966. if !f.validFile(info) {
  967. return nil, fs.ErrorObjectNotFound
  968. }
  969. err = o.setMetaData(info) // sets the info
  970. } else {
  971. err = o.readMetaData(ctx, false) // reads info and meta, returning an error
  972. }
  973. if err != nil {
  974. return nil, err
  975. }
  976. return o, nil
  977. }
  978. // NewObject finds the Object at remote. If it can't be found
  979. // it returns the error fs.ErrorObjectNotFound.
  980. func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  981. return f.newObjectWithInfo(ctx, remote, nil)
  982. }
  983. // CreateDir makes a directory
  984. func (f *Fs) CreateDir(ctx context.Context, path string) (jf *api.JottaFolder, err error) {
  985. // fs.Debugf(f, "CreateDir(%q, %q)\n", pathID, leaf)
  986. var resp *http.Response
  987. opts := rest.Opts{
  988. Method: "POST",
  989. Path: f.filePath(path),
  990. Parameters: url.Values{},
  991. }
  992. opts.Parameters.Set("mkDir", "true")
  993. err = f.pacer.Call(func() (bool, error) {
  994. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &jf)
  995. return shouldRetry(ctx, resp, err)
  996. })
  997. if err != nil {
  998. //fmt.Printf("...Error %v\n", err)
  999. return nil, err
  1000. }
  1001. // fmt.Printf("...Id %q\n", *info.Id)
  1002. return jf, nil
  1003. }
  1004. // List the objects and directories in dir into entries. The
  1005. // entries can be returned in any order but should be for a
  1006. // complete directory.
  1007. //
  1008. // dir should be "" to list the root, and should not have
  1009. // trailing slashes.
  1010. //
  1011. // This should return ErrDirNotFound if the directory isn't
  1012. // found.
  1013. func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  1014. opts := rest.Opts{
  1015. Method: "GET",
  1016. Path: f.filePath(dir),
  1017. }
  1018. var resp *http.Response
  1019. var result api.JottaFolder
  1020. err = f.pacer.Call(func() (bool, error) {
  1021. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
  1022. return shouldRetry(ctx, resp, err)
  1023. })
  1024. if err != nil {
  1025. if apiErr, ok := err.(*api.Error); ok {
  1026. // does not exist
  1027. if apiErr.StatusCode == http.StatusNotFound {
  1028. return nil, fs.ErrorDirNotFound
  1029. }
  1030. }
  1031. return nil, fmt.Errorf("couldn't list files: %w", err)
  1032. }
  1033. if !f.validFolder(&result) {
  1034. return nil, fs.ErrorDirNotFound
  1035. }
  1036. for i := range result.Folders {
  1037. item := &result.Folders[i]
  1038. if f.validFolder(item) {
  1039. remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
  1040. d := fs.NewDir(remote, time.Time(item.ModifiedAt))
  1041. entries = append(entries, d)
  1042. }
  1043. }
  1044. for i := range result.Files {
  1045. item := &result.Files[i]
  1046. if f.validFile(item) {
  1047. remote := path.Join(dir, f.opt.Enc.ToStandardName(item.Name))
  1048. if o, err := f.newObjectWithInfo(ctx, remote, item); err == nil {
  1049. entries = append(entries, o)
  1050. }
  1051. }
  1052. }
  1053. return entries, nil
  1054. }
  1055. func parseListRStream(ctx context.Context, r io.Reader, filesystem *Fs, callback func(fs.DirEntry) error) error {
  1056. type stats struct {
  1057. Folders int `xml:"folders"`
  1058. Files int `xml:"files"`
  1059. }
  1060. var expected, actual stats
  1061. type xmlFile struct {
  1062. Path string `xml:"path"`
  1063. Name string `xml:"filename"`
  1064. Checksum string `xml:"md5"`
  1065. Size int64 `xml:"size"`
  1066. Modified api.Rfc3339Time `xml:"modified"` // Note: Liststream response includes 3 decimal milliseconds, but we ignore them since there is second precision everywhere else
  1067. Created api.Rfc3339Time `xml:"created"`
  1068. }
  1069. type xmlFolder struct {
  1070. Path string `xml:"path"`
  1071. }
  1072. addFolder := func(path string) error {
  1073. return callback(fs.NewDir(filesystem.opt.Enc.ToStandardPath(path), time.Time{}))
  1074. }
  1075. addFile := func(f *xmlFile) error {
  1076. return callback(&Object{
  1077. hasMetaData: true,
  1078. fs: filesystem,
  1079. remote: filesystem.opt.Enc.ToStandardPath(path.Join(f.Path, f.Name)),
  1080. size: f.Size,
  1081. md5: f.Checksum,
  1082. createTime: time.Time(f.Created),
  1083. modTime: time.Time(f.Modified),
  1084. })
  1085. }
  1086. // liststream paths are /mountpoint/root/path
  1087. // so the returned paths should have /mountpoint/root/ trimmed
  1088. // as the caller is expecting path.
  1089. pathPrefix := filesystem.opt.Enc.FromStandardPath(path.Join("/", filesystem.opt.Mountpoint, filesystem.root))
  1090. trimPathPrefix := func(p string) string {
  1091. p = strings.TrimPrefix(p, pathPrefix)
  1092. p = strings.TrimPrefix(p, "/")
  1093. return p
  1094. }
  1095. uniqueFolders := map[string]bool{}
  1096. decoder := xml.NewDecoder(r)
  1097. for {
  1098. t, err := decoder.Token()
  1099. if err != nil {
  1100. if err != io.EOF {
  1101. return err
  1102. }
  1103. break
  1104. }
  1105. switch se := t.(type) {
  1106. case xml.StartElement:
  1107. switch se.Name.Local {
  1108. case "file":
  1109. var f xmlFile
  1110. if err := decoder.DecodeElement(&f, &se); err != nil {
  1111. return err
  1112. }
  1113. f.Path = trimPathPrefix(f.Path)
  1114. actual.Files++
  1115. if !uniqueFolders[f.Path] {
  1116. uniqueFolders[f.Path] = true
  1117. actual.Folders++
  1118. if err := addFolder(f.Path); err != nil {
  1119. return err
  1120. }
  1121. }
  1122. if err := addFile(&f); err != nil {
  1123. return err
  1124. }
  1125. case "folder":
  1126. var f xmlFolder
  1127. if err := decoder.DecodeElement(&f, &se); err != nil {
  1128. return err
  1129. }
  1130. f.Path = trimPathPrefix(f.Path)
  1131. uniqueFolders[f.Path] = true
  1132. actual.Folders++
  1133. if err := addFolder(f.Path); err != nil {
  1134. return err
  1135. }
  1136. case "stats":
  1137. if err := decoder.DecodeElement(&expected, &se); err != nil {
  1138. return err
  1139. }
  1140. }
  1141. }
  1142. }
  1143. if expected.Folders != actual.Folders ||
  1144. expected.Files != actual.Files {
  1145. return fmt.Errorf("invalid result from listStream: expected[%#v] != actual[%#v]", expected, actual)
  1146. }
  1147. return nil
  1148. }
  1149. // ListR lists the objects and directories of the Fs starting
  1150. // from dir recursively into out.
  1151. //
  1152. // dir should be "" to start from the root, and should not
  1153. // have trailing slashes.
  1154. func (f *Fs) ListR(ctx context.Context, dir string, callback fs.ListRCallback) (err error) {
  1155. opts := rest.Opts{
  1156. Method: "GET",
  1157. Path: f.filePath(dir),
  1158. Parameters: url.Values{},
  1159. }
  1160. opts.Parameters.Set("mode", "liststream")
  1161. list := walk.NewListRHelper(callback)
  1162. var resp *http.Response
  1163. err = f.pacer.Call(func() (bool, error) {
  1164. resp, err = f.jfsSrv.Call(ctx, &opts)
  1165. if err != nil {
  1166. return shouldRetry(ctx, resp, err)
  1167. }
  1168. err = parseListRStream(ctx, resp.Body, f, func(d fs.DirEntry) error {
  1169. if d.Remote() == dir {
  1170. return nil
  1171. }
  1172. return list.Add(d)
  1173. })
  1174. _ = resp.Body.Close()
  1175. return shouldRetry(ctx, resp, err)
  1176. })
  1177. if err != nil {
  1178. if apiErr, ok := err.(*api.Error); ok {
  1179. // does not exist
  1180. if apiErr.StatusCode == http.StatusNotFound {
  1181. return fs.ErrorDirNotFound
  1182. }
  1183. }
  1184. return fmt.Errorf("couldn't list files: %w", err)
  1185. }
  1186. if err != nil {
  1187. return err
  1188. }
  1189. return list.Flush()
  1190. }
  1191. // Creates from the parameters passed in a half finished Object which
  1192. // must have setMetaData called on it
  1193. //
  1194. // Used to create new objects
  1195. func (f *Fs) createObject(remote string, modTime time.Time, size int64) (o *Object) {
  1196. // Temporary Object under construction
  1197. o = &Object{
  1198. fs: f,
  1199. remote: remote,
  1200. size: size,
  1201. modTime: modTime,
  1202. }
  1203. return o
  1204. }
  1205. // Put the object
  1206. //
  1207. // Copy the reader in to the new object which is returned.
  1208. //
  1209. // The new object may have been created if an error is returned
  1210. func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  1211. o := f.createObject(src.Remote(), src.ModTime(ctx), src.Size())
  1212. return o, o.Update(ctx, in, src, options...)
  1213. }
  1214. // mkParentDir makes the parent of the native path dirPath if
  1215. // necessary and any directories above that
  1216. func (f *Fs) mkParentDir(ctx context.Context, dirPath string) error {
  1217. // defer log.Trace(dirPath, "")("")
  1218. // chop off trailing / if it exists
  1219. parent := path.Dir(strings.TrimSuffix(dirPath, "/"))
  1220. if parent == "." {
  1221. parent = ""
  1222. }
  1223. return f.Mkdir(ctx, parent)
  1224. }
  1225. // Mkdir creates the container if it doesn't exist
  1226. func (f *Fs) Mkdir(ctx context.Context, dir string) error {
  1227. _, err := f.CreateDir(ctx, dir)
  1228. return err
  1229. }
  1230. // purgeCheck removes the root directory, if check is set then it
  1231. // refuses to do so if it has anything in
  1232. func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) (err error) {
  1233. root := path.Join(f.root, dir)
  1234. if root == "" {
  1235. return errors.New("can't purge root directory")
  1236. }
  1237. // check that the directory exists
  1238. entries, err := f.List(ctx, dir)
  1239. if err != nil {
  1240. return err
  1241. }
  1242. if check {
  1243. if len(entries) != 0 {
  1244. return fs.ErrorDirectoryNotEmpty
  1245. }
  1246. }
  1247. opts := rest.Opts{
  1248. Method: "POST",
  1249. Path: f.filePath(dir),
  1250. Parameters: url.Values{},
  1251. NoResponse: true,
  1252. }
  1253. if f.opt.HardDelete {
  1254. opts.Parameters.Set("rmDir", "true")
  1255. } else {
  1256. opts.Parameters.Set("dlDir", "true")
  1257. }
  1258. var resp *http.Response
  1259. err = f.pacer.Call(func() (bool, error) {
  1260. resp, err = f.jfsSrv.Call(ctx, &opts)
  1261. return shouldRetry(ctx, resp, err)
  1262. })
  1263. if err != nil {
  1264. return fmt.Errorf("couldn't purge directory: %w", err)
  1265. }
  1266. return nil
  1267. }
  1268. // Rmdir deletes the root folder
  1269. //
  1270. // Returns an error if it isn't empty
  1271. func (f *Fs) Rmdir(ctx context.Context, dir string) error {
  1272. return f.purgeCheck(ctx, dir, true)
  1273. }
  1274. // Precision return the precision of this Fs
  1275. func (f *Fs) Precision() time.Duration {
  1276. return time.Second
  1277. }
  1278. // Purge deletes all the files and the container
  1279. func (f *Fs) Purge(ctx context.Context, dir string) error {
  1280. return f.purgeCheck(ctx, dir, false)
  1281. }
  1282. // createOrUpdate tries to make remote file match without uploading.
  1283. // If the remote file exists, and has matching size and md5, only
  1284. // timestamps are updated. If the file does not exist or does does
  1285. // not match size and md5, but matching content can be constructed
  1286. // from deduplication, the file will be updated/created. If the file
  1287. // is currently in trash, but can be made to match, it will be
  1288. // restored. Returns ErrorObjectNotFound if upload will be necessary
  1289. // to get a matching remote file.
  1290. func (f *Fs) createOrUpdate(ctx context.Context, file string, createTime time.Time, modTime time.Time, size int64, md5 string) (info *api.JottaFile, err error) {
  1291. opts := rest.Opts{
  1292. Method: "POST",
  1293. Path: f.filePath(file),
  1294. Parameters: url.Values{},
  1295. ExtraHeaders: make(map[string]string),
  1296. }
  1297. opts.Parameters.Set("cphash", "true")
  1298. opts.ExtraHeaders["JSize"] = strconv.FormatInt(size, 10)
  1299. opts.ExtraHeaders["JMd5"] = md5
  1300. opts.ExtraHeaders["JCreated"] = api.JottaTime(createTime).String()
  1301. opts.ExtraHeaders["JModified"] = api.JottaTime(modTime).String()
  1302. var resp *http.Response
  1303. err = f.pacer.Call(func() (bool, error) {
  1304. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &info)
  1305. return shouldRetry(ctx, resp, err)
  1306. })
  1307. if apiErr, ok := err.(*api.Error); ok {
  1308. // does not exist, i.e. not matching size and md5, and not possible to make it by deduplication
  1309. if apiErr.StatusCode == http.StatusNotFound {
  1310. return nil, fs.ErrorObjectNotFound
  1311. }
  1312. }
  1313. return info, nil
  1314. }
  1315. // copyOrMoves copies or moves directories or files depending on the method parameter
  1316. func (f *Fs) copyOrMove(ctx context.Context, method, src, dest string) (info *api.JottaFile, err error) {
  1317. opts := rest.Opts{
  1318. Method: "POST",
  1319. Path: src,
  1320. Parameters: url.Values{},
  1321. }
  1322. opts.Parameters.Set(method, f.filePathRaw(dest, true))
  1323. var resp *http.Response
  1324. err = f.pacer.Call(func() (bool, error) {
  1325. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &info)
  1326. return shouldRetry(ctx, resp, err)
  1327. })
  1328. if err != nil {
  1329. return nil, err
  1330. }
  1331. return info, nil
  1332. }
  1333. // Copy src to this remote using server-side copy operations.
  1334. //
  1335. // This is stored with the remote path given.
  1336. //
  1337. // It returns the destination Object and a possible error.
  1338. //
  1339. // Will only be called if src.Fs().Name() == f.Name()
  1340. //
  1341. // If it isn't possible then return fs.ErrorCantCopy
  1342. func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1343. srcObj, ok := src.(*Object)
  1344. if !ok {
  1345. fs.Debugf(src, "Can't copy - not same remote type")
  1346. return nil, fs.ErrorCantMove
  1347. }
  1348. meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx))
  1349. if err != nil {
  1350. return nil, err
  1351. }
  1352. if err := f.mkParentDir(ctx, remote); err != nil {
  1353. return nil, err
  1354. }
  1355. info, err := f.copyOrMove(ctx, "cp", srcObj.filePath(), remote)
  1356. if err == nil {
  1357. var createTime time.Time
  1358. var createTimeMeta bool
  1359. var modTime time.Time
  1360. var modTimeMeta bool
  1361. if meta != nil {
  1362. createTime, createTimeMeta = srcObj.parseFsMetadataTime(meta, "btime")
  1363. if !createTimeMeta {
  1364. createTime = srcObj.createTime
  1365. }
  1366. modTime, modTimeMeta = srcObj.parseFsMetadataTime(meta, "mtime")
  1367. if !modTimeMeta {
  1368. modTime = srcObj.modTime
  1369. }
  1370. }
  1371. if bool(info.Deleted) && !f.opt.TrashedOnly && info.State == "COMPLETED" {
  1372. // Workaround necessary when destination was a trashed file, to avoid the copied file also being in trash (bug in api?)
  1373. fs.Debugf(src, "Server-side copied to trashed destination, restoring")
  1374. info, err = f.createOrUpdate(ctx, remote, createTime, modTime, info.Size, info.MD5)
  1375. } else if createTimeMeta || modTimeMeta {
  1376. info, err = f.createOrUpdate(ctx, remote, createTime, modTime, info.Size, info.MD5)
  1377. }
  1378. }
  1379. if err != nil {
  1380. return nil, fmt.Errorf("couldn't copy file: %w", err)
  1381. }
  1382. return f.newObjectWithInfo(ctx, remote, info)
  1383. //return f.newObjectWithInfo(remote, &result)
  1384. }
  1385. // Move src to this remote using server-side move operations.
  1386. //
  1387. // This is stored with the remote path given.
  1388. //
  1389. // It returns the destination Object and a possible error.
  1390. //
  1391. // Will only be called if src.Fs().Name() == f.Name()
  1392. //
  1393. // If it isn't possible then return fs.ErrorCantMove
  1394. func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1395. srcObj, ok := src.(*Object)
  1396. if !ok {
  1397. fs.Debugf(src, "Can't move - not same remote type")
  1398. return nil, fs.ErrorCantMove
  1399. }
  1400. meta, err := fs.GetMetadataOptions(ctx, f, src, fs.MetadataAsOpenOptions(ctx))
  1401. if err != nil {
  1402. return nil, err
  1403. }
  1404. if err := f.mkParentDir(ctx, remote); err != nil {
  1405. return nil, err
  1406. }
  1407. info, err := f.copyOrMove(ctx, "mv", srcObj.filePath(), remote)
  1408. if err == nil && meta != nil {
  1409. createTime, createTimeMeta := srcObj.parseFsMetadataTime(meta, "btime")
  1410. if !createTimeMeta {
  1411. createTime = srcObj.createTime
  1412. }
  1413. modTime, modTimeMeta := srcObj.parseFsMetadataTime(meta, "mtime")
  1414. if !modTimeMeta {
  1415. modTime = srcObj.modTime
  1416. }
  1417. if createTimeMeta || modTimeMeta {
  1418. info, err = f.createOrUpdate(ctx, remote, createTime, modTime, info.Size, info.MD5)
  1419. }
  1420. }
  1421. if err != nil {
  1422. return nil, fmt.Errorf("couldn't move file: %w", err)
  1423. }
  1424. return f.newObjectWithInfo(ctx, remote, info)
  1425. //return f.newObjectWithInfo(remote, result)
  1426. }
  1427. // DirMove moves src, srcRemote to this remote at dstRemote
  1428. // using server-side move operations.
  1429. //
  1430. // Will only be called if src.Fs().Name() == f.Name()
  1431. //
  1432. // If it isn't possible then return fs.ErrorCantDirMove
  1433. //
  1434. // If destination exists then return fs.ErrorDirExists
  1435. func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
  1436. srcFs, ok := src.(*Fs)
  1437. if !ok {
  1438. fs.Debugf(srcFs, "Can't move directory - not same remote type")
  1439. return fs.ErrorCantDirMove
  1440. }
  1441. srcPath := path.Join(srcFs.root, srcRemote)
  1442. dstPath := path.Join(f.root, dstRemote)
  1443. // Refuse to move to or from the root
  1444. if srcPath == "" || dstPath == "" {
  1445. fs.Debugf(src, "DirMove error: Can't move root")
  1446. return errors.New("can't move root directory")
  1447. }
  1448. //fmt.Printf("Move src: %s (FullPath %s), dst: %s (FullPath: %s)\n", srcRemote, srcPath, dstRemote, dstPath)
  1449. var err error
  1450. _, err = f.List(ctx, dstRemote)
  1451. if err == fs.ErrorDirNotFound {
  1452. // OK
  1453. } else if err != nil {
  1454. return err
  1455. } else {
  1456. return fs.ErrorDirExists
  1457. }
  1458. _, err = f.copyOrMove(ctx, "mvDir", path.Join(f.fileEndpoint, f.opt.Enc.FromStandardPath(srcPath))+"/", dstRemote)
  1459. if err != nil {
  1460. return fmt.Errorf("couldn't move directory: %w", err)
  1461. }
  1462. return nil
  1463. }
  1464. // PublicLink generates a public link to the remote path (usually readable by anyone)
  1465. func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (link string, err error) {
  1466. opts := rest.Opts{
  1467. Method: "GET",
  1468. Path: f.filePath(remote),
  1469. Parameters: url.Values{},
  1470. }
  1471. if unlink {
  1472. opts.Parameters.Set("mode", "disableShare")
  1473. } else {
  1474. opts.Parameters.Set("mode", "enableShare")
  1475. }
  1476. var resp *http.Response
  1477. var result api.JottaFile
  1478. err = f.pacer.Call(func() (bool, error) {
  1479. resp, err = f.jfsSrv.CallXML(ctx, &opts, nil, &result)
  1480. return shouldRetry(ctx, resp, err)
  1481. })
  1482. if apiErr, ok := err.(*api.Error); ok {
  1483. // does not exist
  1484. if apiErr.StatusCode == http.StatusNotFound {
  1485. return "", fs.ErrorObjectNotFound
  1486. }
  1487. }
  1488. if err != nil {
  1489. if unlink {
  1490. return "", fmt.Errorf("couldn't remove public link: %w", err)
  1491. }
  1492. return "", fmt.Errorf("couldn't create public link: %w", err)
  1493. }
  1494. if unlink {
  1495. if result.PublicURI != "" {
  1496. return "", fmt.Errorf("couldn't remove public link - %q", result.PublicURI)
  1497. }
  1498. return "", nil
  1499. }
  1500. if result.PublicURI == "" {
  1501. return "", errors.New("couldn't create public link - no uri received")
  1502. }
  1503. if result.PublicSharePath != "" {
  1504. webLink := joinPath(wwwURL, result.PublicSharePath)
  1505. fs.Debugf(nil, "Web link: %s", webLink)
  1506. } else {
  1507. fs.Debugf(nil, "No web link received")
  1508. }
  1509. directLink := joinPath(wwwURL, fmt.Sprintf("opin/io/downloadPublic/%s/%s", f.user, result.PublicURI))
  1510. fs.Debugf(nil, "Direct link: %s", directLink)
  1511. return directLink, nil
  1512. }
  1513. // About gets quota information
  1514. func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
  1515. info, err := getDriveInfo(ctx, f.jfsSrv, f.user)
  1516. if err != nil {
  1517. return nil, err
  1518. }
  1519. usage := &fs.Usage{
  1520. Used: fs.NewUsageValue(info.Usage),
  1521. }
  1522. if info.Capacity > 0 {
  1523. usage.Total = fs.NewUsageValue(info.Capacity)
  1524. usage.Free = fs.NewUsageValue(info.Capacity - info.Usage)
  1525. }
  1526. return usage, nil
  1527. }
  1528. // UserInfo fetches info about the current user
  1529. func (f *Fs) UserInfo(ctx context.Context) (userInfo map[string]string, err error) {
  1530. cust, err := getCustomerInfo(ctx, f.apiSrv)
  1531. if err != nil {
  1532. return nil, err
  1533. }
  1534. return map[string]string{
  1535. "Username": cust.Username,
  1536. "Email": cust.Email,
  1537. "Name": cust.Name,
  1538. "AccountType": cust.AccountType,
  1539. "SubscriptionType": cust.SubscriptionType,
  1540. }, nil
  1541. }
  1542. // CleanUp empties the trash
  1543. func (f *Fs) CleanUp(ctx context.Context) error {
  1544. opts := rest.Opts{
  1545. Method: "POST",
  1546. Path: "files/v1/purge_trash",
  1547. }
  1548. var info api.TrashResponse
  1549. _, err := f.apiSrv.CallJSON(ctx, &opts, nil, &info)
  1550. if err != nil {
  1551. return fmt.Errorf("couldn't empty trash: %w", err)
  1552. }
  1553. return nil
  1554. }
  1555. // Shutdown shutdown the fs
  1556. func (f *Fs) Shutdown(ctx context.Context) error {
  1557. f.tokenRenewer.Shutdown()
  1558. return nil
  1559. }
  1560. // Hashes returns the supported hash sets.
  1561. func (f *Fs) Hashes() hash.Set {
  1562. return hash.Set(hash.MD5)
  1563. }
  1564. // ---------------------------------------------
  1565. // Fs returns the parent Fs
  1566. func (o *Object) Fs() fs.Info {
  1567. return o.fs
  1568. }
  1569. // Return a string version
  1570. func (o *Object) String() string {
  1571. if o == nil {
  1572. return "<nil>"
  1573. }
  1574. return o.remote
  1575. }
  1576. // Remote returns the remote path
  1577. func (o *Object) Remote() string {
  1578. return o.remote
  1579. }
  1580. // filePath returns an escaped file path (f.root, remote)
  1581. func (o *Object) filePath() string {
  1582. return o.fs.filePath(o.remote)
  1583. }
  1584. // Hash returns the MD5 of an object returning a lowercase hex string
  1585. func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
  1586. if t != hash.MD5 {
  1587. return "", hash.ErrUnsupported
  1588. }
  1589. return o.md5, nil
  1590. }
  1591. // Size returns the size of an object in bytes
  1592. func (o *Object) Size() int64 {
  1593. ctx := context.TODO()
  1594. err := o.readMetaData(ctx, false)
  1595. if err != nil {
  1596. fs.Logf(o, "Failed to read metadata: %v", err)
  1597. return 0
  1598. }
  1599. return o.size
  1600. }
  1601. // MimeType of an Object if known, "" otherwise
  1602. func (o *Object) MimeType(ctx context.Context) string {
  1603. return o.mimeType
  1604. }
  1605. // validFile checks if info indicates file is valid
  1606. func (f *Fs) validFile(info *api.JottaFile) bool {
  1607. if info.State != "COMPLETED" {
  1608. return false // File is incomplete or corrupt
  1609. }
  1610. if !info.Deleted {
  1611. return !f.opt.TrashedOnly // Regular file; return false if TrashedOnly, else true
  1612. }
  1613. return f.opt.TrashedOnly // Deleted file; return true if TrashedOnly, else false
  1614. }
  1615. // validFolder checks if info indicates folder is valid
  1616. func (f *Fs) validFolder(info *api.JottaFolder) bool {
  1617. // Returns true if folder is not deleted.
  1618. // If TrashedOnly option then always returns true, because a folder not
  1619. // in trash must be traversed to get to files/subfolders that are.
  1620. return !bool(info.Deleted) || f.opt.TrashedOnly
  1621. }
  1622. // setMetaData sets the metadata from info
  1623. func (o *Object) setMetaData(info *api.JottaFile) (err error) {
  1624. o.hasMetaData = true
  1625. o.size = info.Size
  1626. o.md5 = info.MD5
  1627. o.mimeType = info.MimeType
  1628. o.createTime = time.Time(info.CreatedAt)
  1629. o.modTime = time.Time(info.ModifiedAt)
  1630. o.updateTime = time.Time(info.UpdatedAt)
  1631. return nil
  1632. }
  1633. // readMetaData reads and updates the metadata for an object
  1634. func (o *Object) readMetaData(ctx context.Context, force bool) (err error) {
  1635. if o.hasMetaData && !force {
  1636. return nil
  1637. }
  1638. info, err := o.fs.readMetaDataForPath(ctx, o.remote)
  1639. if err != nil {
  1640. return err
  1641. }
  1642. if !o.fs.validFile(info) {
  1643. return fs.ErrorObjectNotFound
  1644. }
  1645. return o.setMetaData(info)
  1646. }
  1647. // parseFsMetadataTime parses a time string from fs.Metadata with key
  1648. func (o *Object) parseFsMetadataTime(m fs.Metadata, key string) (t time.Time, ok bool) {
  1649. value, ok := m[key]
  1650. if ok {
  1651. var err error
  1652. t, err = time.Parse(time.RFC3339Nano, value) // metadata stores RFC3339Nano timestamps
  1653. if err != nil {
  1654. fs.Debugf(o, "failed to parse metadata %s: %q: %v", key, value, err)
  1655. ok = false
  1656. }
  1657. }
  1658. return t, ok
  1659. }
  1660. // ModTime returns the modification time of the object
  1661. //
  1662. // It attempts to read the objects mtime and if that isn't present the
  1663. // LastModified returned in the http headers
  1664. func (o *Object) ModTime(ctx context.Context) time.Time {
  1665. err := o.readMetaData(ctx, false)
  1666. if err != nil {
  1667. fs.Logf(o, "Failed to read metadata: %v", err)
  1668. return time.Now()
  1669. }
  1670. return o.modTime
  1671. }
  1672. // SetModTime sets the modification time of the local fs object
  1673. func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1674. // make sure metadata is available, we need its current size and md5
  1675. err := o.readMetaData(ctx, false)
  1676. if err != nil {
  1677. fs.Logf(o, "Failed to read metadata: %v", err)
  1678. return err
  1679. }
  1680. // request check/update with existing metadata and new modtime
  1681. // (note that if size/md5 does not match, the file content will
  1682. // also be modified if deduplication is possible, i.e. it is
  1683. // important to use correct/latest values)
  1684. _, err = o.fs.createOrUpdate(ctx, o.remote, o.createTime, modTime, o.size, o.md5)
  1685. if err != nil {
  1686. if err == fs.ErrorObjectNotFound {
  1687. // file was modified (size/md5 changed) between readMetaData and createOrUpdate?
  1688. return errors.New("metadata did not match")
  1689. }
  1690. return err
  1691. }
  1692. // update local metadata
  1693. o.modTime = modTime
  1694. return nil
  1695. }
  1696. // Storable returns a boolean showing whether this object storable
  1697. func (o *Object) Storable() bool {
  1698. return true
  1699. }
  1700. // Open an object for read
  1701. func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1702. fs.FixRangeOption(options, o.size)
  1703. var resp *http.Response
  1704. opts := rest.Opts{
  1705. Method: "GET",
  1706. Path: o.filePath(),
  1707. Parameters: url.Values{},
  1708. Options: options,
  1709. }
  1710. opts.Parameters.Set("mode", "bin")
  1711. err = o.fs.pacer.Call(func() (bool, error) {
  1712. resp, err = o.fs.jfsSrv.Call(ctx, &opts)
  1713. return shouldRetry(ctx, resp, err)
  1714. })
  1715. if err != nil {
  1716. return nil, err
  1717. }
  1718. return resp.Body, err
  1719. }
  1720. // Read the md5 of in returning a reader which will read the same contents
  1721. //
  1722. // The cleanup function should be called when out is finished with
  1723. // regardless of whether this function returned an error or not.
  1724. func readMD5(in io.Reader, size, threshold int64) (md5sum string, out io.Reader, cleanup func(), err error) {
  1725. // we need an MD5
  1726. md5Hasher := md5.New()
  1727. // use the teeReader to write to the local file AND calculate the MD5 while doing so
  1728. teeReader := io.TeeReader(in, md5Hasher)
  1729. // nothing to clean up by default
  1730. cleanup = func() {}
  1731. // don't cache small files on disk to reduce wear of the disk
  1732. if size > threshold {
  1733. var tempFile *os.File
  1734. // create the cache file
  1735. tempFile, err = os.CreateTemp("", cachePrefix)
  1736. if err != nil {
  1737. return
  1738. }
  1739. _ = os.Remove(tempFile.Name()) // Delete the file - may not work on Windows
  1740. // clean up the file after we are done downloading
  1741. cleanup = func() {
  1742. // the file should normally already be close, but just to make sure
  1743. _ = tempFile.Close()
  1744. _ = os.Remove(tempFile.Name()) // delete the cache file after we are done - may be deleted already
  1745. }
  1746. // copy the ENTIRE file to disc and calculate the MD5 in the process
  1747. if _, err = io.Copy(tempFile, teeReader); err != nil {
  1748. return
  1749. }
  1750. // jump to the start of the local file so we can pass it along
  1751. if _, err = tempFile.Seek(0, 0); err != nil {
  1752. return
  1753. }
  1754. // replace the already read source with a reader of our cached file
  1755. out = tempFile
  1756. } else {
  1757. // that's a small file, just read it into memory
  1758. var inData []byte
  1759. inData, err = io.ReadAll(teeReader)
  1760. if err != nil {
  1761. return
  1762. }
  1763. // set the reader to our read memory block
  1764. out = bytes.NewReader(inData)
  1765. }
  1766. return hex.EncodeToString(md5Hasher.Sum(nil)), out, cleanup, nil
  1767. }
  1768. // Update the object with the contents of the io.Reader, modTime and size
  1769. //
  1770. // If existing is set then it updates the object rather than creating a new one.
  1771. //
  1772. // The new object may have been created if an error is returned
  1773. func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (err error) {
  1774. if o.fs.opt.NoVersions {
  1775. err := o.readMetaData(ctx, false)
  1776. if err == nil {
  1777. // if the object exists delete it
  1778. err = o.remove(ctx, true)
  1779. if err != nil && err != fs.ErrorObjectNotFound {
  1780. // if delete failed then report that, unless it was because the file did not exist after all
  1781. return fmt.Errorf("failed to remove old object: %w", err)
  1782. }
  1783. } else if err != fs.ErrorObjectNotFound {
  1784. // if the object does not exist we can just continue but if the error is something different we should report that
  1785. return err
  1786. }
  1787. }
  1788. o.fs.tokenRenewer.Start()
  1789. defer o.fs.tokenRenewer.Stop()
  1790. size := src.Size()
  1791. md5String, err := src.Hash(ctx, hash.MD5)
  1792. if err != nil || md5String == "" {
  1793. // unwrap the accounting from the input, we use wrap to put it
  1794. // back on after the buffering
  1795. var wrap accounting.WrapFn
  1796. in, wrap = accounting.UnWrap(in)
  1797. var cleanup func()
  1798. md5String, in, cleanup, err = readMD5(in, size, int64(o.fs.opt.MD5MemoryThreshold))
  1799. defer cleanup()
  1800. if err != nil {
  1801. return fmt.Errorf("failed to calculate MD5: %w", err)
  1802. }
  1803. // Wrap the accounting back onto the stream
  1804. in = wrap(in)
  1805. }
  1806. // Fetch metadata if --metadata is in use
  1807. meta, err := fs.GetMetadataOptions(ctx, o.fs, src, options)
  1808. if err != nil {
  1809. return fmt.Errorf("failed to read metadata from source object: %w", err)
  1810. }
  1811. var createdTime string
  1812. var modTime string
  1813. if meta != nil {
  1814. if t, ok := o.parseFsMetadataTime(meta, "btime"); ok {
  1815. createdTime = api.Rfc3339Time(t).String() // jottacloud api wants RFC3339 timestamps
  1816. }
  1817. if t, ok := o.parseFsMetadataTime(meta, "mtime"); ok {
  1818. modTime = api.Rfc3339Time(t).String()
  1819. }
  1820. }
  1821. if modTime == "" { // prefer mtime in meta as Modified time, fallback to source ModTime
  1822. modTime = api.Rfc3339Time(src.ModTime(ctx)).String()
  1823. }
  1824. if createdTime == "" { // if no Created time set same as Modified
  1825. createdTime = modTime
  1826. }
  1827. // use the api to allocate the file first and get resume / deduplication info
  1828. var resp *http.Response
  1829. opts := rest.Opts{
  1830. Method: "POST",
  1831. Path: "files/v1/allocate",
  1832. Options: options,
  1833. ExtraHeaders: make(map[string]string),
  1834. }
  1835. // the allocate request
  1836. var request = api.AllocateFileRequest{
  1837. Bytes: size,
  1838. Created: createdTime,
  1839. Modified: modTime,
  1840. Md5: md5String,
  1841. Path: o.fs.allocatePathRaw(o.remote, true),
  1842. }
  1843. // send it
  1844. var response api.AllocateFileResponse
  1845. err = o.fs.pacer.CallNoRetry(func() (bool, error) {
  1846. resp, err = o.fs.apiSrv.CallJSON(ctx, &opts, &request, &response)
  1847. return shouldRetry(ctx, resp, err)
  1848. })
  1849. if err != nil {
  1850. return err
  1851. }
  1852. // If the file state is INCOMPLETE and CORRUPT, we must upload it.
  1853. // Else, if the file state is COMPLETE, we don't need to upload it because
  1854. // the content is already there, possibly it was created with deduplication,
  1855. // and also any metadata changes are already performed by the allocate request.
  1856. if response.State != "COMPLETED" {
  1857. // how much do we still have to upload?
  1858. remainingBytes := size - response.ResumePos
  1859. opts = rest.Opts{
  1860. Method: "POST",
  1861. RootURL: response.UploadURL,
  1862. ContentLength: &remainingBytes,
  1863. ContentType: "application/octet-stream",
  1864. Body: in,
  1865. ExtraHeaders: make(map[string]string),
  1866. }
  1867. if response.ResumePos != 0 {
  1868. opts.ExtraHeaders["Range"] = "bytes=" + strconv.FormatInt(response.ResumePos, 10) + "-" + strconv.FormatInt(size-1, 10)
  1869. }
  1870. // copy the already uploaded bytes into the trash :)
  1871. var result api.UploadResponse
  1872. _, err = io.CopyN(io.Discard, in, response.ResumePos)
  1873. if err != nil {
  1874. return err
  1875. }
  1876. // send the remaining bytes
  1877. _, err = o.fs.apiSrv.CallJSON(ctx, &opts, nil, &result)
  1878. if err != nil {
  1879. return err
  1880. }
  1881. // Upload response contains main metadata properties (size, md5 and modTime)
  1882. // which could be set back to the object, but it does not contain the
  1883. // necessary information to set the createTime and updateTime properties,
  1884. // so must therefore perform a read instead.
  1885. }
  1886. // in any case we must update the object meta data
  1887. return o.readMetaData(ctx, true)
  1888. }
  1889. func (o *Object) remove(ctx context.Context, hard bool) error {
  1890. opts := rest.Opts{
  1891. Method: "POST",
  1892. Path: o.filePath(),
  1893. Parameters: url.Values{},
  1894. NoResponse: true,
  1895. }
  1896. if hard {
  1897. opts.Parameters.Set("rm", "true")
  1898. } else {
  1899. opts.Parameters.Set("dl", "true")
  1900. }
  1901. err := o.fs.pacer.Call(func() (bool, error) {
  1902. resp, err := o.fs.jfsSrv.CallXML(ctx, &opts, nil, nil)
  1903. return shouldRetry(ctx, resp, err)
  1904. })
  1905. if apiErr, ok := err.(*api.Error); ok {
  1906. // attempting to hard delete will fail if path does not exist, but standard delete will succeed
  1907. if apiErr.StatusCode == http.StatusNotFound {
  1908. return fs.ErrorObjectNotFound
  1909. }
  1910. }
  1911. return err
  1912. }
  1913. // Remove an object
  1914. func (o *Object) Remove(ctx context.Context) error {
  1915. return o.remove(ctx, o.fs.opt.HardDelete)
  1916. }
  1917. // Metadata returns metadata for an object
  1918. //
  1919. // It should return nil if there is no Metadata
  1920. func (o *Object) Metadata(ctx context.Context) (metadata fs.Metadata, err error) {
  1921. err = o.readMetaData(ctx, false)
  1922. if err != nil {
  1923. fs.Logf(o, "Failed to read metadata: %v", err)
  1924. return nil, err
  1925. }
  1926. metadata.Set("btime", o.createTime.Format(time.RFC3339Nano)) // metadata timestamps should be RFC3339Nano
  1927. metadata.Set("mtime", o.modTime.Format(time.RFC3339Nano))
  1928. metadata.Set("utime", o.updateTime.Format(time.RFC3339Nano))
  1929. metadata.Set("content-type", o.mimeType)
  1930. return metadata, nil
  1931. }
  1932. // Check the interfaces are satisfied
  1933. var (
  1934. _ fs.Fs = (*Fs)(nil)
  1935. _ fs.Purger = (*Fs)(nil)
  1936. _ fs.Copier = (*Fs)(nil)
  1937. _ fs.Mover = (*Fs)(nil)
  1938. _ fs.DirMover = (*Fs)(nil)
  1939. _ fs.ListRer = (*Fs)(nil)
  1940. _ fs.PublicLinker = (*Fs)(nil)
  1941. _ fs.Abouter = (*Fs)(nil)
  1942. _ fs.UserInfoer = (*Fs)(nil)
  1943. _ fs.CleanUpper = (*Fs)(nil)
  1944. _ fs.Shutdowner = (*Fs)(nil)
  1945. _ fs.Object = (*Object)(nil)
  1946. _ fs.MimeTyper = (*Object)(nil)
  1947. _ fs.Metadataer = (*Object)(nil)
  1948. )