Archived
Edit configuration in GUI; use XML configuration
This commit is contained in:
1 parent
71def3a970
commit
a1d575894a
15 files changed
+578
-255
No files matched your search
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
package auto
|
package auto
|
||||||
|
|
||||||
import "github.com/cratonica/embed"
|
import "github.com/cratonica/embed"
|
||||||
|
|
||||||
var Resources embed.PackedResourceMap = embed.PackedResourceMap{0x1f, 0x8b, 0x8, 0x0, 0x0, 0x9, 0x6e, 0x88, 0x2, 0xff, 0xbc, 0xfd, 0xd9, 0x7f, 0xdb, 0xb8, 0xb2, 0x2f, 0x8e, 0xee, 0xfb, 0xaa, 0x87, 0xfb, 0x7a, 0x5f, 0x6d, 0x6e, 0x1f, 0x35, 0x19, 0xc1, 0xb2, 0x9d, 0xee, 0xbd, 0x6, 0xd2, 0x8c, 0x8e, 0xe3, 0x38, 0x53, 0xc7, 0x4e, 0x3a, 0x76, 0xd2, 0x83, 0xac, 0xee, 0x1f, 0x38, 0x68, 0x68, 0x8d, 0xd1, 0xe0, 0xc4, 0xb1, 0xf5, 0x7e, 0xff, 0xec, 0x5f, 0x7d, 0xb, 0x3, 0x41, 0x89, 0x4e, 0xaf, 0xb5, 0xcf, 0xfe, 0x9c, 0x1e, 0x2c, 0x12, 0x4, 0x1, 0x10, 0x28, 0x14, 0x6a, 0xae, 0xff, 0xef, 0x7f, 0xfc, 0xc7, 0x7f, 0xc8, 0x49, 0x6f, 0x35, 0x92, 0xf3, 0xe6, 0x78, 0x30, 0x69, 0xfe, 0xb9, 0xf8, 0xff, 0xfd, 0xff, 0xff, 0x3f, 0xff, 0x71, 0xf0, 0xa8, 0xb6, 0x73, 0xa2, 0x4a, 0x5f, 0x5f, 0xee, 0xdc, 0x1c, 0x35, 0x1f, 0x37, 0xff, 0x5e, 0xdb, 0xf1, 0xd3, 0x60, 0xe7, 0xf1, 0xe1, 0xd1, 0xe1, 0x3e, 0xfd, 0xf9, 0x61, 0xe7, 0xc5, 0x74, 0xda, 0x1b, 0xe5, 0x62, 0xe7, 0xd5, 0x24, 0x6d, 0xee, 0xf4, 0x97, 0xcb, 0x59, 0x78, 0x70, 0xa0, 0x5b, 0xfa, 0x73, 0xd1, 0x9c, 0xce, 0x7b, 0xb5, 0x9d, 0x37, 0x83, 0x34, 0x9f, 0x2c, 0xf2, 0x70, 0xe7, 0xfc, 0xd5, 0x55, 0xed, 0xd1, 0x41, 0xcd, 0xef, 0xae, 0x26, 0xe9, 0x72, 0x30, 0x9d, 0xf8, 0xbf, 0x89, 0x9f, 0xc4, 0x3c, 0xb8, 0xfb, 0x6e, 0xb5, 0xc8, 0x77, 0x16, 0xcb, 0xf9, 0x20, 0x5d, 0x7e, 0x17, 0x99, 0x87, 0x3b, 0xcf, 0xfd, 0x24, 0xb8, 0x9b, 0xe7, 0xcb, 0xd5, 0x7c, 0xb2, 0x63, 0xdf, 0x8, 0xee, 0x6e, 0xe4, 0x7c, 0x47, 0xc6, 0x72, 0xde, 0x5b, 0x8d, 0xf3, 0xc9, 0x72, 0xd1, 0x3e, 0xec, 0x88, 0x54, 0xc8, 0xd8, 0x6b, 0x7b, 0xd, 0x3f, 0x69, 0x25, 0xd, 0x2f, 0xa4, 0x7f, 0xbd, 0xa0, 0x21, 0x1b, 0x5e, 0xc7, 0xc, 0x28, 0x9f, 0xcf, 0xa7, 0xf3, 0x45, 0xb3, 0x34, 0xae, 0x3, 0xfe, 0x9c, 0x3, 0xf3, 0xd6, 0x81, 0x7e, 0x2b, 0xea, 0x4e, 0xe7, 0x7e, 0x1a, 0x1f, 0x45, 0xe9, 0xb1, 0xed, 0xa3, 0x39, 0xca, 0x27, 0xbd, 0x65, 0x3f, 0x4a, 0x1b, 0x8d, 0x80, 0xba, 0x6e, 0xf8, 0x47, 0x71, 0x9c, 0xb6, 0xbc, 0x16, 0xbd, 0x52, 0xa7, 0x77, 0xbc, 0x19, 0x35, 0x92, 0xee, 0x1f, 0xd1, 0x55, 0xec, 0x35, 0xf2, 0x49, 0x3a, 0xcd, 0xf2, 0xf, 0xef, 0x5f, 0x9d, 0x4e, 0xc7, 0xb3, 0xe9, 0x84, 0xde, 0xf7, 0x3d, 0x33, 0x7c, 0x2f, 0x8e, 0x97, 0xb7, 0xb3, 0x7c, 0xda, 0xdd, 0x29, 0xc6, 0x9f, 0x76, 0x5a, 0xee, 0x4d, 0x73, 0x39, 0xbd, 0xa4, 0x99, 0x98, 0xf4, 0xfc, 0xa0, 0x39, 0xcf, 0x67, 0x23, 0x99, 0xe6, 0xfe, 0xc1, 0xce, 0xf5, 0x5d, 0xfb, 0x7a, 0x71, 0x7d, 0xd9, 0x79, 0xb4, 0x77, 0x20, 0x68, 0x98, 0xa1, 0xb7, 0x9a, 0x64, 0x79, 0x77, 0x30, 0xc9, 0xb3, 0x7, 0x9a, 0x74, 0x2a, 0x84, 0xde, 0x82, 0x1b, 0xf4, 0x76, 0x2b, 0x6b, 0xbe, 0xbe, 0x7c, 0x7b, 0xd1, 0x54, 0x35, 0x6, 0xdd, 0x5b, 0xdf, 0x7d, 0x16, 0x84, 0xa5, 0xbb, 0x48, 0x2f, 0xc6, 0x19, 0x66, 0xd3, 0x97, 0xc1, 0x7a, 0x6d, 0xd7, 0xea, 0x53, 0x82, 0xc5, 0x1a, 0x74, 0xfd, 0xc9, 0x6a, 0x34, 0x8a, 0xe3, 0xe4, 0xfe, 0xfe, 0xab, 0xa4, 0x92, 0x40, 0xbd, 0xb1, 0x7b, 0x14, 0xa9, 0x55, 0xab, 0x25, 0x66, 0x2a, 0x75, 0x53, 0x34, 0x91, 0x71, 0xd2, 0x9c, 0xd0, 0x84, 0x5d, 0xd1, 0xd0, 0xea, 0x75, 0xd9, 0xda, 0x3d, 0xc, 0x9f, 0xd1, 0x9b, 0xf7, 0xf7, 0x3f, 0xf2, 0xdf, 0x43, 0xaa, 0x20, 0xef, 0xef, 0xbd, 0xc9, 0x6a, 0x9c, 0xe4, 0x73, 0xfa, 0x58, 0xfb, 0xd, 0xf5, 0xfa, 0xe1, 0x31, 0xfd, 0x91, 0xfb, 0x47, 0x3b, 0x83, 0xc9, 0x4e, 0xe2, 0x8c, 0xc5, 0x4f, 0x84, 0x14, 0xa9, 0x82, 0x94, 0x2c, 0xa2, 0x41, 0x25, 0x1, 0xfd, 0x79, 0x83, 0xf1, 0x60, 0x71, 0x33, 0xae, 0x1f, 0x78, 0xb3, 0xf9, 0x74, 0x39, 0x45, 0x6b, 0xd4, 0x6a, 0x76, 0x7f, 0xef, 0x7b, 0x6a, 0x68, 0xea, 0xce, 0x9b, 0xc8, 0xb1, 0x7e, 0x90, 0x34, 0xfb, 0x72, 0xf1, 0xf6, 0xf3, 0xe4, 0xdd, 0x7c, 0x3a, 0xcb, 0xe7, 0xcb, 0xdb, 0x7a, 0x7d, 0x77, 0xb3, 0xc8, 0xcf, 0x2, 0x1a, 0xab, 0x6c, 0xa6, 0x72, 0x34, 0xf2, 0x53, 0x91, 0xb4, 0xb3, 0x8e, 0xc8, 0x82, 0x28, 0x1f, 0x11, 0x5c, 0xa3, 0xff, 0x26, 0xf5, 0x7b, 0x26, 0xd3, 0x7e, 0xbd, 0x6e, 0x2f, 0x77, 0xe3, 0xf8, 0x53, 0x60, 0xef, 0x7c, 0xc, 0xd8, 0xd6, 0xe7, 0xd9, 0x54, 0x63, 0x8d, 0xf, 0xa3, 0xec, 0xd8, 0xce, 0x5a, 0x6, 0x0, 0xac, 0xec, 0xc5, 0xf9, 0xb0, 0x8a, 0xc1, 0xd1, 0x34, 0x6d, 0xbe, 0xa5, 0x17, 0xc0, 0x99, 0xb7, 0xb7, 0xbc, 0x86, 0x6a, 0xa1, 0xda, 0xb4, 0xa9, 0xd4, 0x4e, 0x78, 0xa0, 0xcd, 0x94, 0xdb, 0x9c, 0xad, 0x16, 0x7d, 0xba, 0x34, 0x8d, 0xc9, 0xe6, 0x62, 0x3a, 0x5f, 0xfa, 0x81, 0xd3, 0x66, 0x4a, 0x8b, 0x51, 0xe3, 0xd5, 0x40, 0x6b, 0xbc, 0x22, 0x31, 0x77, 0x24, 0x72, 0xfa, 0xb4, 0xfc, 0x38, 0x33, 0x9f, 0x96, 0x6f, 0x7c, 0x5a, 0x3b, 0xef, 0xd0, 0x38, 0xe9, 0xaf, 0x6d, 0x3d, 0x2b, 0x9a, 0x7d, 0x97, 0x54, 0xe1, 0x6, 0xee, 0x26, 0xa1, 0xd7, 0x4b, 0xa0, 0xf9, 0xab, 0xf4, 0x8b, 0xce, 0x93, 0x78, 0x28, 0x75, 0x8f, 0x42, 0x46, 0x49, 0x44, 0xf5, 0xf7, 0xf7, 0x23, 0x49, 0xa5, 0xed, 0xa4, 0xd3, 0x4c, 0xfb, 0x72, 0x7e, 0x4a, 0xb0, 0x78, 0xb2, 0xf4, 0xf, 0x3, 0x0, 0xce, 0x7f, 0xfd, 0x9d, 0xa0, 0x4f, 0x3, 0xf1, 0xe, 0xd7, 0x89, 0xbd, 0x13, 0x4f, 0x50, 0x1b, 0x7f, 0x4e, 0x7, 0x13, 0x9f, 0x76, 0x22, 0x6a, 0xfd, 0xf3, 0x10, 0xb5, 0xf4, 0xe3, 0x43, 0x4f, 0x2d, 0x48, 0xe9, 0x25, 0xb5, 0xa3, 0x9b, 0xdd, 0xf9, 0x74, 0x7c, 0xaa, 0xfb, 0xf0, 0x65, 0xe3, 0x28, 0x70, 0x5b, 0x5a, 0xd3, 0xf5, 0x6a, 0xb2, 0xe8, 0xf, 0xba, 0x84, 0x30, 0xe, 0x3d, 0xfb, 0xdLine truncated
|
var Resources embed.PackedResourceMap = embed.PackedResourceMap{0x1f, 0x8b, 0x8, 0x0, 0x0, 0x9, 0x6e, 0x88, 0x2, 0xff, 0xbc, 0xfd, 0xd9, 0x7f, 0xdb, 0xb8, 0xb2, 0x2f, 0x8e, 0xee, 0xfb, 0xaa, 0x87, 0xfb, 0x7a, 0x5f, 0x6d, 0x6e, 0x1f, 0x35, 0x19, 0xc1, 0xb2, 0x9d, 0xee, 0xbd, 0x6, 0xd2, 0x8c, 0x8e, 0xe3, 0x38, 0x53, 0xc7, 0x4e, 0x3a, 0x76, 0xd2, 0x83, 0xac, 0xee, 0x1f, 0x38, 0x68, 0x68, 0x8d, 0xd1, 0xe0, 0xc4, 0xb1, 0xf5, 0x7e, 0xff, 0xec, 0x5f, 0x7d, 0xb, 0x3, 0x41, 0x89, 0x4e, 0xaf, 0xb5, 0xcf, 0xfe, 0x9c, 0x1e, 0x2c, 0x12, 0x4, 0x1, 0x10, 0x28, 0x14, 0x6a, 0xae, 0xff, 0xef, 0x7f, 0xfc, 0xc7, 0x7f, 0xc8, 0x49, 0x6f, 0x35, 0x92, 0xf3, 0xe6, 0x78, 0x30, 0x69, 0xfe, 0xb9, 0xf8, 0xff, 0xfd, 0xff, 0xff, 0x3f, 0xff, 0x71, 0xf0, 0xa8, 0xb6, 0x73, 0xa2, 0x4a, 0x5f, 0x5f, 0xee, 0xdc, 0x1c, 0x35, 0x1f, 0x37, 0xff, 0x5e, 0xdb, 0xf1, 0xd3, 0x60, 0xe7, 0xf1, 0xe1, 0xd1, 0xe1, 0x3e, 0xfd, 0xf9, 0x61, 0xe7, 0xc5, 0x74, 0xda, 0x1b, 0xe5, 0x62, 0xe7, 0xd5, 0x24, 0x6d, 0xee, 0xf4, 0x97, 0xcb, 0x59, 0x78, 0x70, 0xa0, 0x5b, 0xfa, 0x73, 0xd1, 0x9c, 0xce, 0x7b, 0xb5, 0x9d, 0x37, 0x83, 0x34, 0x9f, 0x2c, 0xf2, 0x70, 0xe7, 0xfc, 0xd5, 0x55, 0xed, 0xd1, 0x41, 0xcd, 0xef, 0xae, 0x26, 0xe9, 0x72, 0x30, 0x9d, 0xf8, 0xbf, 0x89, 0x9f, 0xc4, 0x3c, 0xb8, 0xfb, 0x6e, 0xb5, 0xc8, 0x77, 0x16, 0xcb, 0xf9, 0x20, 0x5d, 0x7e, 0x17, 0x99, 0x87, 0x3b, 0xcf, 0xfd, 0x24, 0xb8, 0x9b, 0xe7, 0xcb, 0xd5, 0x7c, 0xb2, 0x63, 0xdf, 0x8, 0xee, 0x6e, 0xe4, 0x7c, 0x47, 0xc6, 0x72, 0xde, 0x5b, 0x8d, 0xf3, 0xc9, 0x72, 0xd1, 0x3e, 0xec, 0x88, 0x54, 0xc8, 0xd8, 0x6b, 0x7b, 0xd, 0x3f, 0x69, 0x25, 0xd, 0x2f, 0xa4, 0x7f, 0xbd, 0xa0, 0x21, 0x1b, 0x5e, 0xc7, 0xc, 0x28, 0x9f, 0xcf, 0xa7, 0xf3, 0x45, 0xb3, 0x34, 0xae, 0x3, 0xfe, 0x9c, 0x3, 0xf3, 0xd6, 0x81, 0x7e, 0x2b, 0xea, 0x4e, 0xe7, 0x7e, 0x1a, 0x1f, 0x45, 0xe9, 0xb1, 0xed, 0xa3, 0x39, 0xca, 0x27, 0xbd, 0x65, 0x3f, 0x4a, 0x1b, 0x8d, 0x80, 0xba, 0x6e, 0xf8, 0x47, 0x71, 0x9c, 0xb6, 0xbc, 0x16, 0xbd, 0x52, 0xa7, 0x77, 0xbc, 0x19, 0x35, 0x92, 0xee, 0x1f, 0xd1, 0x55, 0xec, 0x35, 0xf2, 0x49, 0x3a, 0xcd, 0xf2, 0xf, 0xef, 0x5f, 0x9d, 0x4e, 0xc7, 0xb3, 0xe9, 0x84, 0xde, 0xf7, 0x3d, 0x33, 0x7c, 0x2f, 0x8e, 0x97, 0xb7, 0xb3, 0x7c, 0xda, 0xdd, 0x29, 0xc6, 0x9f, 0x76, 0x5a, 0xee, 0x4d, 0x73, 0x39, 0xbd, 0xa4, 0x99, 0x98, 0xf4, 0xfc, 0xa0, 0x39, 0xcf, 0x67, 0x23, 0x99, 0xe6, 0xfe, 0xc1, 0xce, 0xf5, 0x5d, 0xfb, 0x7a, 0x71, 0x7d, 0xd9, 0x79, 0xb4, 0x77, 0x20, 0x68, 0x98, 0xa1, 0xb7, 0x9a, 0x64, 0x79, 0x77, 0x30, 0xc9, 0xb3, 0x7, 0x9a, 0x74, 0x2a, 0x84, 0xde, 0x82, 0x1b, 0xf4, 0x76, 0x2b, 0x6b, 0xbe, 0xbe, 0x7c, 0x7b, 0xd1, 0x54, 0x35, 0x6, 0xdd, 0x5b, 0xdf, 0x7d, 0x16, 0x84, 0xa5, 0xbb, 0x48, 0x2f, 0xc6, 0x19, 0x66, 0xd3, 0x97, 0xc1, 0x7a, 0x6d, 0xd7, 0xea, 0x53, 0x82, 0xc5, 0x1a, 0x74, 0xfd, 0xc9, 0x6a, 0x34, 0x8a, 0xe3, 0xe4, 0xfe, 0xfe, 0xab, 0xa4, 0x92, 0x40, 0xbd, 0xb1, 0x7b, 0x14, 0xa9, 0x55, 0xab, 0x25, 0x66, 0x2a, 0x75, 0x53, 0x34, 0x91, 0x71, 0xd2, 0x9c, 0xd0, 0x84, 0x5d, 0xd1, 0xd0, 0xea, 0x75, 0xd9, 0xda, 0x3d, 0xc, 0x9f, 0xd1, 0x9b, 0xf7, 0xf7, 0x3f, 0xf2, 0xdf, 0x43, 0xaa, 0x20, 0xef, 0xef, 0xbd, 0xc9, 0x6a, 0x9c, 0xe4, 0x73, 0xfa, 0x58, 0xfb, 0xd, 0xf5, 0xfa, 0xe1, 0x31, 0xfd, 0x91, 0xfb, 0x47, 0x3b, 0x83, 0xc9, 0x4e, 0xe2, 0x8c, 0xc5, 0x4f, 0x84, 0x14, 0xa9, 0x82, 0x94, 0x2c, 0xa2, 0x41, 0x25, 0x1, 0xfd, 0x79, 0x83, 0xf1, 0x60, 0x71, 0x33, 0xae, 0x1f, 0x78, 0xb3, 0xf9, 0x74, 0x39, 0x45, 0x6b, 0xd4, 0x6a, 0x76, 0x7f, 0xef, 0x7b, 0x6a, 0x68, 0xea, 0xce, 0x9b, 0xc8, 0xb1, 0x7e, 0x90, 0x34, 0xfb, 0x72, 0xf1, 0xf6, 0xf3, 0xe4, 0xdd, 0x7c, 0x3a, 0xcb, 0xe7, 0xcb, 0xdb, 0x7a, 0x7d, 0x77, 0xb3, 0xc8, 0xcf, 0x2, 0x1a, 0xab, 0x6c, 0xa6, 0x72, 0x34, 0xf2, 0x53, 0x91, 0xb4, 0xb3, 0x8e, 0xc8, 0x82, 0x28, 0x1f, 0x11, 0x5c, 0xa3, 0xff, 0x26, 0xf5, 0x7b, 0x26, 0xd3, 0x7e, 0xbd, 0x6e, 0x2f, 0x77, 0xe3, 0xf8, 0x53, 0x60, 0xef, 0x7c, 0xc, 0xd8, 0xd6, 0xe7, 0xd9, 0x54, 0x63, 0x8d, 0xf, 0xa3, 0xec, 0xd8, 0xce, 0x5a, 0x6, 0x0, 0xac, 0xec, 0xc5, 0xf9, 0xb0, 0x8a, 0xc1, 0xd1, 0x34, 0x6d, 0xbe, 0xa5, 0x17, 0xc0, 0x99, 0xb7, 0xb7, 0xbc, 0x86, 0x6a, 0xa1, 0xda, 0xb4, 0xa9, 0xd4, 0x4e, 0x78, 0xa0, 0xcd, 0x94, 0xdb, 0x9c, 0xad, 0x16, 0x7d, 0xba, 0x34, 0x8d, 0xc9, 0xe6, 0x62, 0x3a, 0x5f, 0xfa, 0x81, 0xd3, 0x66, 0x4a, 0x8b, 0x51, 0xe3, 0xd5, 0x40, 0x6b, 0xbc, 0x22, 0x31, 0x77, 0x24, 0x72, 0xfa, 0xb4, 0xfc, 0x38, 0x33, 0x9f, 0x96, 0x6f, 0x7c, 0x5a, 0x3b, 0xef, 0xd0, 0x38, 0xe9, 0xaf, 0x6d, 0x3d, 0x2b, 0x9a, 0x7d, 0x97, 0x54, 0xe1, 0x6, 0xee, 0x26, 0xa1, 0xd7, 0x4b, 0xa0, 0xf9, 0xab, 0xf4, 0x8b, 0xce, 0x93, 0x78, 0x28, 0x75, 0x8f, 0x42, 0x46, 0x49, 0x44, 0xf5, 0xf7, 0xf7, 0x23, 0x49, 0xa5, 0xed, 0xa4, 0xd3, 0x4c, 0xfb, 0x72, 0x7e, 0x4a, 0xb0, 0x78, 0xb2, 0xf4, 0xf, 0x3, 0x0, 0xce, 0x7f, 0xfd, 0x9d, 0xa0, 0x4f, 0x3, 0xf1, 0xe, 0xd7, 0x89, 0xbd, 0x13, 0x4f, 0x50, 0x1b, 0x7f, 0x4e, 0x7, 0x13, 0x9f, 0x76, 0x22, 0x6a, 0xfd, 0xf3, 0x10, 0xb5, 0xf4, 0xe3, 0x43, 0x4f, 0x2d, 0x48, 0xe9, 0x25, 0xb5, 0xa3, 0x9b, 0xdd, 0xf9, 0x74, 0x7c, 0xaa, 0xfb, 0xf0, 0x65, 0xe3, 0x28, 0x70, 0x5b, 0x5a, 0xd3, 0xf5, 0x6a, 0xb2, 0xe8, 0xf, 0xba, 0x84, 0x30, 0xe, 0x3d, 0xfb, 0xdLine truncated
|
||||||
@@ -1,33 +1,48 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Configuration struct {
|
||||||
Listen string `ini:"listen-address" default:":22000" description:"ip:port to for incoming sync connections"`
|
Version int `xml:"version,attr" default:"1"`
|
||||||
ReadOnly bool `ini:"read-only" description:"Allow changes to the local repository"`
|
Repositories []RepositoryConfiguration `xml:"repository"`
|
||||||
Delete bool `ini:"allow-delete" default:"true" description:"Allow deletes of files in the local repository"`
|
Options OptionsConfiguration `xml:"options"`
|
||||||
Symlinks bool `ini:"follow-symlinks" default:"true" description:"Follow symbolic links at the top level of the repository"`
|
XMLName xml.Name `xml:"configuration" json:"-"`
|
||||||
GUI bool `ini:"gui-enabled" default:"true" description:"Enable the HTTP GUI"`
|
|
||||||
GUIAddr string `ini:"gui-address" default:"127.0.0.1:8080" description:"ip:port for GUI connections"`
|
|
||||||
ExternalServer string `ini:"global-announce-server" default:"syncthing.nym.se:22025" description:"Global server for announcements"`
|
|
||||||
ExternalDiscovery bool `ini:"global-announce-enabled" default:"true" description:"Announce to the global announce server"`
|
|
||||||
LocalDiscovery bool `ini:"local-announce-enabled" default:"true" description:"Announce to the local network"`
|
|
||||||
ParallelRequests int `ini:"parallel-requests" default:"16" description:"Maximum number of blocks to request in parallel"`
|
|
||||||
LimitRate int `ini:"max-send-kbps" description:"Limit outgoing data rate (kbyte/s)"`
|
|
||||||
ScanInterval time.Duration `ini:"rescan-interval" default:"60s" description:"Scan repository for changes this often"`
|
|
||||||
ConnInterval time.Duration `ini:"reconnection-interval" default:"60s" description:"Attempt to (re)connect to peers this often"`
|
|
||||||
MaxChangeBW int `ini:"max-change-bw" default:"1000" description:"Suppress files changing more than this (kbyte/s)"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(m map[string]string, data interface{}) error {
|
type RepositoryConfiguration struct {
|
||||||
|
Directory string `xml:"directory,attr"`
|
||||||
|
Nodes []NodeConfiguration `xml:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeConfiguration struct {
|
||||||
|
NodeID string `xml:"id,attr"`
|
||||||
|
Addresses []string `xml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionsConfiguration struct {
|
||||||
|
ListenAddress string `xml:"listenAddress" default:":22000" ini:"listen-address"`
|
||||||
|
ReadOnly bool `xml:"readOnly" ini:"read-only"`
|
||||||
|
AllowDelete bool `xml:"allowDelete" default:"true" ini:"allow-delete"`
|
||||||
|
FollowSymlinks bool `xml:"followSymlinks" default:"true" ini:"follow-symlinks"`
|
||||||
|
GUIEnabled bool `xml:"guiEnabled" default:"true" ini:"gui-enabled"`
|
||||||
|
GUIAddress string `xml:"guiAddress" default:"127.0.0.1:8080" ini:"gui-address"`
|
||||||
|
GlobalAnnServer string `xml:"globalAnnounceServer" default:"syncthing.nym.se:22025" ini:"global-announce-server"`
|
||||||
|
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true" ini:"global-announce-enabled"`
|
||||||
|
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true" ini:"local-announce-enabled"`
|
||||||
|
ParallelRequests int `xml:"parallelRequests" default:"16" ini:"parallel-requests"`
|
||||||
|
MaxSendKbps int `xml:"maxSendKbps" ini:"max-send-kbps"`
|
||||||
|
RescanIntervalS int `xml:"rescanIntervalS" default:"60" ini:"rescan-interval"`
|
||||||
|
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60" ini:"reconnection-interval"`
|
||||||
|
MaxChangeKbps int `xml:"maxChangeKbps" default:"1000" ini:"max-change-bw"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDefaults(data interface{}) error {
|
||||||
s := reflect.ValueOf(data).Elem()
|
s := reflect.ValueOf(data).Elem()
|
||||||
t := s.Type()
|
t := s.Type()
|
||||||
|
|
||||||
@@ -35,24 +50,9 @@ func loadConfig(m map[string]string, data interface{}) error {
|
|||||||
f := s.Field(i)
|
f := s.Field(i)
|
||||||
tag := t.Field(i).Tag
|
tag := t.Field(i).Tag
|
||||||
|
|
||||||
name := tag.Get("ini")
|
v := tag.Get("default")
|
||||||
if len(name) == 0 {
|
|
||||||
name = strings.ToLower(t.Field(i).Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := m[name]
|
|
||||||
if !ok {
|
|
||||||
v = tag.Get("default")
|
|
||||||
}
|
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
switch f.Interface().(type) {
|
switch f.Interface().(type) {
|
||||||
case time.Duration:
|
|
||||||
d, err := time.ParseDuration(v)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetInt(int64(d))
|
|
||||||
|
|
||||||
case string:
|
case string:
|
||||||
f.SetString(v)
|
f.SetString(v)
|
||||||
|
|
||||||
@@ -74,56 +74,62 @@ func loadConfig(m map[string]string, data interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type cfg struct {
|
func readConfigINI(m map[string]string, data interface{}) error {
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
Comment string
|
|
||||||
}
|
|
||||||
|
|
||||||
func structToValues(data interface{}) []cfg {
|
|
||||||
s := reflect.ValueOf(data).Elem()
|
s := reflect.ValueOf(data).Elem()
|
||||||
t := s.Type()
|
t := s.Type()
|
||||||
|
|
||||||
var vals []cfg
|
|
||||||
for i := 0; i < s.NumField(); i++ {
|
for i := 0; i < s.NumField(); i++ {
|
||||||
f := s.Field(i)
|
f := s.Field(i)
|
||||||
tag := t.Field(i).Tag
|
tag := t.Field(i).Tag
|
||||||
|
|
||||||
var c cfg
|
name := tag.Get("ini")
|
||||||
c.Key = tag.Get("ini")
|
if len(name) == 0 {
|
||||||
if len(c.Key) == 0 {
|
name = strings.ToLower(t.Field(i).Name)
|
||||||
c.Key = strings.ToLower(t.Field(i).Name)
|
}
|
||||||
|
|
||||||
|
if v, ok := m[name]; ok {
|
||||||
|
switch f.Interface().(type) {
|
||||||
|
case string:
|
||||||
|
f.SetString(v)
|
||||||
|
|
||||||
|
case int:
|
||||||
|
i, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
f.SetInt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
f.SetBool(v == "true")
|
||||||
|
|
||||||
|
default:
|
||||||
|
panic(f.Type())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.Value = fmt.Sprint(f.Interface())
|
|
||||||
c.Comment = tag.Get("description")
|
|
||||||
vals = append(vals, c)
|
|
||||||
}
|
}
|
||||||
return vals
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var configTemplateStr = `[repository]
|
func writeConfigXML(wr io.Writer, cfg Configuration) error {
|
||||||
{{if .comments}}; The directory to synchronize. Will be created if it does not exist.
|
e := xml.NewEncoder(wr)
|
||||||
{{end}}dir = {{.dir}}
|
e.Indent("", " ")
|
||||||
|
err := e.Encode(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = wr.Write([]byte("\n"))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
[nodes]
|
func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||||
{{if .comments}}; Map of node ID to addresses, or "dynamic" for automatic discovery. Examples:
|
var cfg Configuration
|
||||||
; J3MZ4G5O4CLHJKB25WX47K5NUJUWDOLO2TTNY3TV3NRU4HVQRKEQ = 172.16.32.24:22000
|
|
||||||
; ZNJZRXQKYHF56A2VVNESRZ6AY4ZOWGFJCV6FXDZJUTRVR3SNBT6Q = dynamic
|
|
||||||
{{end}}{{range $n, $a := .nodes}}{{$n}} = {{$a}}
|
|
||||||
{{end}}
|
|
||||||
[settings]
|
|
||||||
{{range $v := .settings}}; {{$v.Comment}}
|
|
||||||
{{$v.Key}} = {{$v.Value}}
|
|
||||||
{{end}}
|
|
||||||
`
|
|
||||||
|
|
||||||
var configTemplate = template.Must(template.New("config").Parse(configTemplateStr))
|
setDefaults(&cfg)
|
||||||
|
setDefaults(&cfg.Options)
|
||||||
|
|
||||||
func writeConfig(wr io.Writer, dir string, nodes map[string]string, opts Options, comments bool) {
|
var err error
|
||||||
configTemplate.Execute(wr, map[string]interface{}{
|
if rd != nil {
|
||||||
"dir": dir,
|
err = xml.NewDecoder(rd).Decode(&cfg)
|
||||||
"nodes": nodes,
|
}
|
||||||
"settings": structToValues(&opts),
|
|
||||||
"comments": comments,
|
return cfg, err
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -2,19 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"mime"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/calmh/syncthing/auto"
|
|
||||||
"github.com/calmh/syncthing/model"
|
"github.com/calmh/syncthing/model"
|
||||||
"github.com/codegangsta/martini"
|
"github.com/codegangsta/martini"
|
||||||
"github.com/cratonica/embed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func startGUI(addr string, m *model.Model) {
|
func startGUI(addr string, m *model.Model) {
|
||||||
@@ -27,14 +21,11 @@ func startGUI(addr string, m *model.Model) {
|
|||||||
router.Get("/rest/need", restGetNeed)
|
router.Get("/rest/need", restGetNeed)
|
||||||
router.Get("/rest/system", restGetSystem)
|
router.Get("/rest/system", restGetSystem)
|
||||||
|
|
||||||
fs, err := embed.Unpack(auto.Resources)
|
router.Post("/rest/config", restPostConfig)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
mr := martini.New()
|
mr := martini.New()
|
||||||
mr.Use(embeddedStatic(fs))
|
mr.Use(embeddedStatic())
|
||||||
mr.Use(martini.Recovery())
|
mr.Use(martini.Recovery())
|
||||||
mr.Action(router.Handle)
|
mr.Action(router.Handle)
|
||||||
mr.Map(m)
|
mr.Map(m)
|
||||||
@@ -43,7 +34,6 @@ func startGUI(addr string, m *model.Model) {
|
|||||||
warnln("GUI not possible:", err)
|
warnln("GUI not possible:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -80,13 +70,16 @@ func restGetConnections(m *model.Model, w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func restGetConfig(w http.ResponseWriter) {
|
func restGetConfig(w http.ResponseWriter) {
|
||||||
var res = make(map[string]interface{})
|
json.NewEncoder(w).Encode(cfg)
|
||||||
res["myID"] = myID
|
}
|
||||||
res["repository"] = config.OptionMap("repository")
|
|
||||||
res["nodes"] = config.OptionMap("nodes")
|
func restPostConfig(req *http.Request) {
|
||||||
res["nodes"].(map[string]string)[myID] = "self"
|
err := json.NewDecoder(req.Body).Decode(&cfg)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
if err != nil {
|
||||||
json.NewEncoder(w).Encode(res)
|
log.Println(err)
|
||||||
|
} else {
|
||||||
|
saveConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type guiFile model.File
|
type guiFile model.File
|
||||||
@@ -120,6 +113,7 @@ func restGetSystem(w http.ResponseWriter) {
|
|||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
res := make(map[string]interface{})
|
res := make(map[string]interface{})
|
||||||
|
res["myID"] = myID
|
||||||
res["goroutines"] = runtime.NumGoroutine()
|
res["goroutines"] = runtime.NumGoroutine()
|
||||||
res["alloc"] = m.Alloc
|
res["alloc"] = m.Alloc
|
||||||
res["sys"] = m.Sys
|
res["sys"] = m.Sys
|
||||||
@@ -130,29 +124,3 @@ func restGetSystem(w http.ResponseWriter) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func embeddedStatic(fs map[string][]byte) interface{} {
|
|
||||||
var modt = time.Now().UTC().Format(http.TimeFormat)
|
|
||||||
|
|
||||||
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
|
||||||
file := req.URL.Path
|
|
||||||
|
|
||||||
if file[0] == '/' {
|
|
||||||
file = file[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, ok := fs[file]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
|
||||||
if len(mtype) != 0 {
|
|
||||||
res.Header().Set("Content-Type", mtype)
|
|
||||||
}
|
|
||||||
res.Header().Set("Content-Size", fmt.Sprintf("%d", len(bs)))
|
|
||||||
res.Header().Set("Last-Modified", modt)
|
|
||||||
|
|
||||||
res.Write(bs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+171
-4
@@ -4,6 +4,28 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
var prevDate = 0;
|
var prevDate = 0;
|
||||||
var modelGetOK = true;
|
var modelGetOK = true;
|
||||||
|
|
||||||
|
$scope.connections = {};
|
||||||
|
$scope.config = {};
|
||||||
|
$scope.myID = "";
|
||||||
|
$scope.nodes = [];
|
||||||
|
|
||||||
|
// Strings before bools look better
|
||||||
|
$scope.settings = [
|
||||||
|
{id: 'ListenAddress', descr:"Sync Protocol Listen Address", type: 'string', restart: true},
|
||||||
|
{id: 'GUIAddress', descr: "GUI Listen Address", type: 'string', restart: true},
|
||||||
|
{id: 'MaxSendKbps', descr: "Outgoing Rate Limit (KBps)", type: 'string', restart: true},
|
||||||
|
{id: 'RescanIntervalS', descr: "Rescan Interval (s)", type: 'string', restart: true},
|
||||||
|
{id: 'ReconnectIntervalS', descr: "Reconnect Interval (s)", type: 'string', restart: true},
|
||||||
|
{id: 'ParallelRequests', descr: "Max Outstanding Requests", type: 'string', restart: true},
|
||||||
|
{id: 'MaxChangeKbps', descr: "Max File Change Rate (KBps)", type: 'string', restart: true},
|
||||||
|
|
||||||
|
{id: 'ReadOnly', descr: "Read Only", type: 'bool', restart: true},
|
||||||
|
{id: 'AllowDelete', descr: "Allow Delete", type: 'bool', restart: true},
|
||||||
|
{id: 'FollowSymlinks', descr: "Follow Symlinks", type: 'bool', restart: true},
|
||||||
|
{id: 'GlobalAnnEnabled', descr: "Global Announce", type: 'bool', restart: true},
|
||||||
|
{id: 'LocalAnnEnabled', descr: "Local Announce", type: 'bool', restart: true},
|
||||||
|
];
|
||||||
|
|
||||||
function modelGetSucceeded() {
|
function modelGetSucceeded() {
|
||||||
if (!modelGetOK) {
|
if (!modelGetOK) {
|
||||||
$('#networkError').modal('hide');
|
$('#networkError').modal('hide');
|
||||||
@@ -21,8 +43,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
$http.get("/rest/version").success(function (data) {
|
$http.get("/rest/version").success(function (data) {
|
||||||
$scope.version = data;
|
$scope.version = data;
|
||||||
});
|
});
|
||||||
$http.get("/rest/config").success(function (data) {
|
$http.get("/rest/system").success(function (data) {
|
||||||
$scope.config = data;
|
$scope.system = data;
|
||||||
|
$scope.myID = data.myID;
|
||||||
|
|
||||||
|
$http.get("/rest/config").success(function (data) {
|
||||||
|
$scope.config = data;
|
||||||
|
|
||||||
|
var nodes = $scope.config.Repositories[0].Nodes;
|
||||||
|
nodes.sort(function (a, b) {
|
||||||
|
if (a.NodeID == $scope.myID)
|
||||||
|
return -1;
|
||||||
|
if (b.NodeID == $scope.myID)
|
||||||
|
return 1;
|
||||||
|
if (a.NodeID < b.NodeID)
|
||||||
|
return -1;
|
||||||
|
return a.NodeID > b.NodeID;
|
||||||
|
})
|
||||||
|
$scope.nodes = nodes;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.refresh = function () {
|
$scope.refresh = function () {
|
||||||
@@ -70,6 +109,122 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.nodeIcon = function (nodeCfg) {
|
||||||
|
if (nodeCfg.NodeID === $scope.myID) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.connections[nodeCfg.NodeID]) {
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "minus";
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.nodeClass = function (nodeCfg) {
|
||||||
|
if (nodeCfg.NodeID === $scope.myID) {
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn = $scope.connections[nodeCfg.NodeID];
|
||||||
|
if (conn) {
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "info";
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.nodeAddr = function (nodeCfg) {
|
||||||
|
if (nodeCfg.NodeID === $scope.myID) {
|
||||||
|
return "this node";
|
||||||
|
}
|
||||||
|
var conn = $scope.connections[nodeCfg.NodeID];
|
||||||
|
if (conn) {
|
||||||
|
return conn.Address;
|
||||||
|
}
|
||||||
|
return nodeCfg.Addresses.join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.nodeVer = function (nodeCfg) {
|
||||||
|
if (nodeCfg.NodeID === $scope.myID) {
|
||||||
|
return $scope.version;
|
||||||
|
}
|
||||||
|
var conn = $scope.connections[nodeCfg.NodeID];
|
||||||
|
if (conn) {
|
||||||
|
return conn.ClientVersion;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveSettings = function () {
|
||||||
|
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||||
|
$('#settingsTable').collapse('hide');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.editNode = function (nodeCfg) {
|
||||||
|
$scope.currentNode = nodeCfg;
|
||||||
|
$scope.editingExisting = true;
|
||||||
|
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(", ")
|
||||||
|
$('#editNode').modal({backdrop: 'static', keyboard: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addNode = function () {
|
||||||
|
$scope.currentNode = {NodeID: "", AddressesStr: "dynamic"};
|
||||||
|
$scope.editingExisting = false;
|
||||||
|
$('#editNode').modal({backdrop: 'static', keyboard: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.deleteNode = function () {
|
||||||
|
$('#editNode').modal('hide');
|
||||||
|
if (!$scope.editingExisting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newNodes = [];
|
||||||
|
for (var i = 0; i < $scope.nodes.length; i++) {
|
||||||
|
if ($scope.nodes[i].NodeID !== $scope.currentNode.NodeID) {
|
||||||
|
newNodes.push($scope.nodes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.nodes = newNodes;
|
||||||
|
$scope.config.Repositories[0].Nodes = newNodes;
|
||||||
|
|
||||||
|
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.saveNode = function () {
|
||||||
|
$('#editNode').modal('hide');
|
||||||
|
nodeCfg = $scope.currentNode;
|
||||||
|
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
|
||||||
|
|
||||||
|
var done = false;
|
||||||
|
for (var i = 0; i < $scope.nodes.length; i++) {
|
||||||
|
if ($scope.nodes[i].NodeID === nodeCfg.NodeID) {
|
||||||
|
$scope.nodes[i] = nodeCfg;
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!done) {
|
||||||
|
$scope.nodes.push(nodeCfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.nodes.sort(function (a, b) {
|
||||||
|
if (a.NodeID == $scope.myID)
|
||||||
|
return -1;
|
||||||
|
if (b.NodeID == $scope.myID)
|
||||||
|
return 1;
|
||||||
|
if (a.NodeID < b.NodeID)
|
||||||
|
return -1;
|
||||||
|
return a.NodeID > b.NodeID;
|
||||||
|
})
|
||||||
|
|
||||||
|
$scope.config.Repositories[0].Nodes = $scope.nodes;
|
||||||
|
|
||||||
|
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
|
||||||
|
};
|
||||||
|
|
||||||
$scope.refresh();
|
$scope.refresh();
|
||||||
setInterval($scope.refresh, 10000);
|
setInterval($scope.refresh, 10000);
|
||||||
});
|
});
|
||||||
@@ -90,7 +245,7 @@ syncthing.filter('natural', function() {
|
|||||||
syncthing.filter('binary', function() {
|
syncthing.filter('binary', function() {
|
||||||
return function(input) {
|
return function(input) {
|
||||||
if (input === undefined) {
|
if (input === undefined) {
|
||||||
return '- '
|
return '0 '
|
||||||
}
|
}
|
||||||
if (input > 1024 * 1024 * 1024) {
|
if (input > 1024 * 1024 * 1024) {
|
||||||
input /= 1024 * 1024 * 1024;
|
input /= 1024 * 1024 * 1024;
|
||||||
@@ -111,7 +266,7 @@ syncthing.filter('binary', function() {
|
|||||||
syncthing.filter('metric', function() {
|
syncthing.filter('metric', function() {
|
||||||
return function(input) {
|
return function(input) {
|
||||||
if (input === undefined) {
|
if (input === undefined) {
|
||||||
return '- '
|
return '0 '
|
||||||
}
|
}
|
||||||
if (input > 1000 * 1000 * 1000) {
|
if (input > 1000 * 1000 * 1000) {
|
||||||
input /= 1000 * 1000 * 1000;
|
input /= 1000 * 1000 * 1000;
|
||||||
@@ -143,3 +298,15 @@ syncthing.filter('alwaysNumber', function() {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
syncthing.directive('optionEditor', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'C',
|
||||||
|
replace: true,
|
||||||
|
transclude: true,
|
||||||
|
scope: {
|
||||||
|
setting: '=setting',
|
||||||
|
},
|
||||||
|
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
|
||||||
|
};
|
||||||
|
})
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap v3.1.0 (http://getbootstrap.com)
|
||||||
|
* Copyright 2011-2014 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{background-image:-webkit-linear-gradient(top, #fff 0, #e0e0e0 100%);background-image:linear-gradient(to bottom, #fff 0, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#dbdbdb;text-shadow:0 1px 0 #fff;border-color:#ccc}.btn-default:hover,.btn-default:focus{background-color:#e0e0e0;background-position:0 -15px}.btn-default:active,.btn-default.active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-primary{background-image:-webkit-linear-gradient(top, #428bca 0, #2d6ca2 100%);background-image:linear-gradient(to bottom, #428bca 0, #2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:hover,.btn-primary:focus{background-color:#2d6ca2;background-position:0 -15px}.btn-primary:active,.btn-primary.active{background-color:#2d6ca2;border-color:#2b669a}.btn-success{background-image:-webkit-linear-gradient(top, #5cb85c 0, #419641 100%);background-image:linear-gradient(to bottom, #5cb85c 0, #419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:hover,.btn-success:focus{background-color:#419641;background-position:0 -15px}.btn-success:active,.btn-success.active{background-color:#419641;border-color:#3e8f3e}.btn-info{background-image:-webkit-linear-gradient(top, #5bc0de 0, #2aabd2 100%);background-image:linear-gradient(to bottom, #5bc0de 0, #2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:hover,.btn-info:focus{background-color:#2aabd2;background-position:0 -15px}.btn-info:active,.btn-info.active{background-color:#2aabd2;border-color:#28a4c9}.btn-warning{background-image:-webkit-linear-gradient(top, #f0ad4e 0, #eb9316 100%);background-image:linear-gradient(to bottom, #f0ad4e 0, #eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:hover,.btn-warning:focus{background-color:#eb9316;background-position:0 -15px}.btn-warning:active,.btn-warning.active{background-color:#eb9316;border-color:#e38d13}.btn-danger{background-image:-webkit-linear-gradient(top, #d9534f 0, #c12e2a 100%);background-image:linear-gradient(to bottom, #d9534f 0, #c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:hover,.btn-danger:focus{background-color:#c12e2a;background-position:0 -15px}.btn-danger:active,.btn-danger.active{background-color:#c12e2a;border-color:#b92c28}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{background-image:-webkit-linear-gradient(top, #f5f5f5 0, #e8e8e8 100%);background-image:linear-gradient(to bottom, #f5f5f5 0, #e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-color:#e8e8e8}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-image:-webkit-linear-gradient(top, #428bca 0, #357ebd 100%);background-image:linear-gradient(to bottom, #428bca 0, #357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-color:#357ebd}.navbar-default{background-image:-webkit-linear-gradient(top, #fff 0, #f8f8f8 100%);baLine truncated
|
||||||
Vendored
+4
-8
@@ -1,11 +1,7 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap v3.0.3
|
* Bootstrap v3.1.0 (http://getbootstrap.com)
|
||||||
*
|
* Copyright 2011-2014 Twitter, Inc.
|
||||||
* Copyright 2013 Twitter, Inc
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
* Licensed under the Apache License v2.0
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Designed and built with all the love in the world @twitter by @mdo and @fat.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden],template{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a{background:transparent;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace, serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important;} a,a:visited{text-decoration:underline;} a[href]:after{content:" (" attr(href) ")";} abbr[title]:after{content:" (" attr(title) ")";} a[href^="javascript:"]:after,a[href^="#"]:after{content:"";} pre,blockquote{border:1px solid #999;page-break-inside:avoid;} thead{display:table-header-group;} tr,img{page-break-inside:avoid;} img{max-width:100% !important;} @page {margin:2cm .5cm;}p,h2,h3{orphans:3;widows:3;} h2,h3{page-break-after:avoid;} select{background:#fff !important;} .navbar{display:none;} .table td,.table th{background-color:#fff !important;} .btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important;} .label{border:1px solid #000;} .table{border-collapse:collapse !important;} .table-bordered th,.table-bordered td{border:1px solid #ddd !important;}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0, 0, 0, 0);}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:16px;line-height:1.428571429;color:#333333;background-color:#ffffff;}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit;}a{color:#428bca;text-decoration:none;}a:hover,a:focus{color:#2a6496;text-decoration:underline;}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}img{vertical-align:middle;}.img-responsive{display:block;max-width:100%;height:auto;}.img-rounded{border-radius:6px;}.img-thumbnail{padding:4px;line-height:1.428571429;background-color:#ffffff;border:1px solid #dddddd;border-radius:4px;-webkit-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;display:inline-block;max-width:100%;height:auto;}.img-circle{border-radius:50%;}hr{margin-top:22px;margin-bottom:22px;border:0;border-top:1px solid #eeeeee;}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0;}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999999;}h1,h2,h3{margin-top:22px;margin-bottom:11px;}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%;}h4,h5,h6{margin-top:11px;margin-bottom:11px;}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%;}h1,.h1{font-size:41px;}h2,.h2{font-size:34px;}h3,.h3{font-size:28px;}h4,.h4{font-size:20px;}h5,.h5{font-size:16px;}h6,.h6{font-size:14px;}p{margin:0 0 11px;}.lead{margin-bottom:22px;font-size:18px;font-weight:200;line-height:1.4;}@media (min-width:768px){.lead{font-size:24px;}}small,.small{font-size:85%;}cite{font-style:normal;}.text-muted{color:#999999;}.text-primary{color:#428bca;}.text-primary:hover{colLine truncated
|
/*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,.h1,h2,.h2,h3,.h3{margin-top:21px;margin-bottom:10.5px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.leadLine truncated
|
||||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
/*!
|
/*!
|
||||||
* Bootstrap v3.0.3 (http://getbootstrap.com)
|
* Bootstrap v3.1.0 (http://getbootstrap.com)
|
||||||
* Copyright 2013 Twitter, Inc.
|
* Copyright 2011-2014 Twitter, Inc.
|
||||||
* Licensed under http://www.apache.org/licenses/LICENSE-2.0
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if("undefined"==typeof jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]'),b=!0;if(a.length){var c=this.$element.find("input");"radio"===c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?b=!1:a.find(".active").removeClass("active")),b&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}b&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].joiLine truncated
|
+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed.bs.alert").remove()}var c=a(this),d=c.attr("data-target");d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));var e=a(d);b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close.bs.alert"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.one(a.support.transition.end,f).emulateTransitionEnd(150):f()};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.isLoading=!1};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",f.resetText||d.data("resetText",d[e]()),d[e](f[b]||this.options[b]),setTimeout(a.proxy(function(){b=="loadingText"?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},b.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");c.prop("type")=="radio"&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f=typeof c=="object"&&c;e||d.data("bs.button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();if(b>this.$items.length-1||b<0)return;return this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){if(this.sliding)return;return this.slide("next")},b.prototype.prev=function(){if(this.sliding)return;return this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});this.$element.trigger(j);if(j.isDefaultPrevented())return;return this.sliding=!0,f&&this.pause(),this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid.bs.carousel",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid.bs.carousel")},0)}).emulateTransitionEnd(d.css("transition-duration").slice(0,-1)*1e3)):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid.bs.carousel")),f&&this.cycle(),this};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,dLine truncated
|
||||||
+120
-48
@@ -35,6 +35,11 @@ html, body {
|
|||||||
.text-monospace {
|
.text-monospace {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -63,6 +68,60 @@ html, body {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="nodeCfg in nodes" ng-class="{'text-muted': nodeCfg.NodeID == myID}">
|
||||||
|
<td>
|
||||||
|
<span class="label label-{{nodeClass(nodeCfg)}}">
|
||||||
|
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="text-monospace">{{nodeCfg.NodeID | short}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{nodeVer(nodeCfg)}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{nodeAddr(nodeCfg)}}
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<span ng-show="nodeCfg.NodeID != myID">
|
||||||
|
<abbr title="{{connections[nodeCfg.NodeID].InBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].inbps | metric}}bps</abbr>
|
||||||
|
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<span ng-show="nodeCfg.NodeID != myID">
|
||||||
|
<abbr title="{{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].outbps | metric}}bps</abbr>
|
||||||
|
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<button ng-show="nodeCfg.NodeID != myID" type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="text-right">
|
||||||
|
<button type="button" class="btn btn-default btn-xs" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
@@ -75,7 +134,9 @@ html, body {
|
|||||||
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
|
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
<div class="panel-heading"><h3 class="panel-title">System</h3></div>
|
<div class="panel-heading"><h3 class="panel-title">System</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -83,57 +144,34 @@ html, body {
|
|||||||
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
|
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="model.needFiles > 0">
|
|
||||||
<h2>Files to Synchronize</h2>
|
|
||||||
<table class="table table-condensed table-striped">
|
|
||||||
<tr ng-repeat="file in need track by $index">
|
|
||||||
<td><abbr title="{{file.Name}}">{{file.ShortName}}</abbr></td>
|
|
||||||
<td class="text-right">{{file.Size | binary}}B</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel panel-info">
|
<div class="panel panel-info">
|
||||||
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
|
<div class="panel-heading"><h3 class="panel-title"><a href="" data-toggle="collapse" data-target="#settingsTable">Settings</a></h3></div>
|
||||||
<table class="table table-condensed">
|
<div id="settingsTable" class="panel-collapse collapse">
|
||||||
<tbody>
|
<div class="panel-body">
|
||||||
<tr ng-repeat="(node, address) in config.nodes" ng-class="{'text-primary': !!connections[node], 'text-muted': node == config.myID}">
|
<form role="form">
|
||||||
<td><span class="text-monospace">{{node | short}}</span></td>
|
<div class="form-group" ng-repeat="setting in settings">
|
||||||
<td>
|
<div ng-if="setting.type == 'string'">
|
||||||
<span ng-show="node != config.myID">{{connections[node].ClientVersion}}</span>
|
<label for="{{setting.id}}">{{setting.descr}}</label>
|
||||||
<span ng-show="node == config.myID">{{version}}</span>
|
<input id="{{setting.id}}" class="form-control" type="text" ng-model="config.Options[setting.id]"></input>
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||||
<span ng-show="node == config.myID">
|
<label>
|
||||||
<span class="glyphicon glyphicon-ok"></span>
|
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
|
||||||
(this node)
|
</label>
|
||||||
</span>
|
</div>
|
||||||
<span ng-show="node != config.myID && !!connections[node]">
|
</div>
|
||||||
<span class="glyphicon glyphicon-link"></span>
|
</form>
|
||||||
{{connections[node].Address}}
|
</div>
|
||||||
</span>
|
<div class="panel-footer">
|
||||||
<span ng-show="node != config.myID && !connections[node]">
|
<button type="button" class="btn btn-sm btn-default" ng-click="saveSettings()">Save</button>
|
||||||
<span class="glyphicon glyphicon-cog"></span>
|
<small><span class="text-muted">Changes take effect when restarting syncthing.</span></small>
|
||||||
{{address}}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</td>
|
|
||||||
<td class="text-right">
|
|
||||||
<span ng-show="node != config.myID">
|
|
||||||
<abbr title="{{connections[node].InBytesTotal | binary}}B">{{connections[node].inbps | metric}}b/s</abbr>
|
|
||||||
<span class="text-muted glyphicon glyphicon-cloud-download"></span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">
|
|
||||||
<span ng-show="node != config.myID">
|
|
||||||
<abbr title="{{connections[node].OutBytesTotal | binary}}B">{{connections[node].outbps | metric}}b/s</abbr>
|
|
||||||
<span class="text-muted glyphicon glyphicon-cloud-upload"></span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,6 +204,40 @@ html, body {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="editNode" class="modal fade">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Edit Node</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form role="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="nodeID">Node ID</label>
|
||||||
|
<input placeholder="YUFJOUDPORCMA..." ng-disabled="editingExisting" id="nodeID" class="form-control" type="text" ng-model="currentNode.NodeID"></input>
|
||||||
|
<p class="help-block">The node ID can be found in the logs or in the "Add Node" dialog on the other node.</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="addresses">Addresses</label>
|
||||||
|
<input placeholder="dynamic" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
|
||||||
|
<p class="help-block">Enter comma separated <span class="text-monospace">ip:port</span> addresses or <span class="text-monospace">dynamic</span> to perform automatic discovery of the address.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div ng-show="!editingExisting">
|
||||||
|
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
|
||||||
|
<pre>{{myID}}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="saveNode()">Save</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="angular.min.js"></script>
|
<script src="angular.min.js"></script>
|
||||||
<script src="jquery-2.0.3.min.js"></script>
|
<script src="jquery-2.0.3.min.js"></script>
|
||||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//+build guidev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/codegangsta/martini"
|
||||||
|
|
||||||
|
func embeddedStatic() interface{} {
|
||||||
|
return martini.Static("gui")
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
//+build !guidev
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/calmh/syncthing/auto"
|
||||||
|
"github.com/cratonica/embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func embeddedStatic() interface{} {
|
||||||
|
fs, err := embed.Unpack(auto.Resources)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var modt = time.Now().UTC().Format(http.TimeFormat)
|
||||||
|
|
||||||
|
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
||||||
|
file := req.URL.Path
|
||||||
|
|
||||||
|
if file[0] == '/' {
|
||||||
|
file = file[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
bs, ok := fs[file]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||||
|
if len(mtype) != 0 {
|
||||||
|
res.Header().Set("Content-Type", mtype)
|
||||||
|
}
|
||||||
|
res.Header().Set("Content-Size", fmt.Sprintf("%d", len(bs)))
|
||||||
|
res.Header().Set("Last-Modified", modt)
|
||||||
|
|
||||||
|
res.Write(bs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,17 +23,12 @@ import (
|
|||||||
"github.com/calmh/syncthing/protocol"
|
"github.com/calmh/syncthing/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
var opts Options
|
var cfg Configuration
|
||||||
var Version string = "unknown-dev"
|
var Version string = "unknown-dev"
|
||||||
|
|
||||||
const (
|
|
||||||
confFileName = "syncthing.ini"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
myID string
|
myID string
|
||||||
config ini.Config
|
config ini.Config
|
||||||
nodeAddrs = make(map[string][]string)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -89,46 +84,75 @@ func main() {
|
|||||||
log.SetPrefix("[" + myID[0:5] + "] ")
|
log.SetPrefix("[" + myID[0:5] + "] ")
|
||||||
logger.SetPrefix("[" + myID[0:5] + "] ")
|
logger.SetPrefix("[" + myID[0:5] + "] ")
|
||||||
|
|
||||||
|
// Prepare to be able to save configuration
|
||||||
|
|
||||||
|
cfgFile := path.Join(confDir, "config.xml")
|
||||||
|
go saveConfigLoop(cfgFile)
|
||||||
|
|
||||||
// Load the configuration file, if it exists.
|
// Load the configuration file, if it exists.
|
||||||
// If it does not, create a template.
|
// If it does not, create a template.
|
||||||
|
|
||||||
cfgFile := path.Join(confDir, confFileName)
|
|
||||||
cf, err := os.Open(cfgFile)
|
cf, err := os.Open(cfgFile)
|
||||||
|
if err == nil {
|
||||||
if err != nil {
|
// Read config.xml
|
||||||
infoln("My ID:", myID)
|
cfg, err = readConfigXML(cf)
|
||||||
|
|
||||||
infoln("No config file; creating a template")
|
|
||||||
|
|
||||||
loadConfig(nil, &opts) //loads defaults
|
|
||||||
fd, err := os.Create(cfgFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalln(err)
|
fatalln(err)
|
||||||
}
|
}
|
||||||
|
cf.Close()
|
||||||
|
} else {
|
||||||
|
// No config.xml, let's try the old syncthing.ini
|
||||||
|
iniFile := path.Join(confDir, "syncthing.ini")
|
||||||
|
cf, err := os.Open(iniFile)
|
||||||
|
if err == nil {
|
||||||
|
infoln("Migrating syncthing.ini to config.xml")
|
||||||
|
iniCfg := ini.Parse(cf)
|
||||||
|
cf.Close()
|
||||||
|
os.Rename(iniFile, path.Join(confDir, "migrated_syncthing.ini"))
|
||||||
|
|
||||||
writeConfig(fd, "~/Sync", map[string]string{myID: "dynamic"}, opts, true)
|
cfg, _ = readConfigXML(nil)
|
||||||
fd.Close()
|
cfg.Repositories = []RepositoryConfiguration{
|
||||||
infof("Edit %s to suit and restart syncthing.", cfgFile)
|
{Directory: iniCfg.Get("repository", "dir")},
|
||||||
|
}
|
||||||
|
readConfigINI(iniCfg.OptionMap("settings"), &cfg.Options)
|
||||||
|
for name, addrs := range iniCfg.OptionMap("nodes") {
|
||||||
|
n := NodeConfiguration{
|
||||||
|
NodeID: name,
|
||||||
|
Addresses: strings.Fields(addrs),
|
||||||
|
}
|
||||||
|
cfg.Repositories[0].Nodes = append(cfg.Repositories[0].Nodes, n)
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
saveConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config = ini.Parse(cf)
|
if len(cfg.Repositories) == 0 {
|
||||||
cf.Close()
|
infoln("No config file; starting with empty defaults")
|
||||||
|
|
||||||
loadConfig(config.OptionMap("settings"), &opts)
|
cfg, err = readConfigXML(nil)
|
||||||
|
cfg.Repositories = []RepositoryConfiguration{
|
||||||
|
{
|
||||||
|
Directory: "~/Sync",
|
||||||
|
Nodes: []NodeConfiguration{
|
||||||
|
{NodeID: myID, Addresses: []string{"dynamic"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
saveConfig()
|
||||||
|
infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
if showConfig {
|
if showConfig {
|
||||||
writeConfig(os.Stdout,
|
writeConfigXML(os.Stdout, cfg)
|
||||||
config.Get("repository", "dir"),
|
|
||||||
config.OptionMap("nodes"), opts, false)
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
infoln("Version", Version)
|
infoln("Version", Version)
|
||||||
infoln("My ID:", myID)
|
infoln("My ID:", myID)
|
||||||
|
|
||||||
var dir = expandTilde(config.Get("repository", "dir"))
|
var dir = expandTilde(cfg.Repositories[0].Directory)
|
||||||
if len(dir) == 0 {
|
if len(dir) == 0 {
|
||||||
fatalln("No repository directory. Set dir under [repository] in syncthing.ini.")
|
fatalln("No repository directory. Set dir under [repository] in syncthing.ini.")
|
||||||
}
|
}
|
||||||
@@ -145,7 +169,7 @@ func main() {
|
|||||||
// The TLS configuration is used for both the listening socket and outgoing
|
// The TLS configuration is used for both the listening socket and outgoing
|
||||||
// connections.
|
// connections.
|
||||||
|
|
||||||
cfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
NextProtos: []string{"bep/1.0"},
|
NextProtos: []string{"bep/1.0"},
|
||||||
ServerName: myID,
|
ServerName: myID,
|
||||||
@@ -155,35 +179,27 @@ func main() {
|
|||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a map of desired node connections based on the configuration file
|
|
||||||
// directives.
|
|
||||||
|
|
||||||
for nodeID, addrs := range config.OptionMap("nodes") {
|
|
||||||
addrs := strings.Fields(addrs)
|
|
||||||
nodeAddrs[nodeID] = addrs
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureDir(dir, -1)
|
ensureDir(dir, -1)
|
||||||
m := model.NewModel(dir, opts.MaxChangeBW*1000)
|
m := model.NewModel(dir, cfg.Options.MaxChangeKbps*1000)
|
||||||
for _, t := range strings.Split(trace, ",") {
|
for _, t := range strings.Split(trace, ",") {
|
||||||
m.Trace(t)
|
m.Trace(t)
|
||||||
}
|
}
|
||||||
if opts.LimitRate > 0 {
|
if cfg.Options.MaxSendKbps > 0 {
|
||||||
m.LimitRate(opts.LimitRate)
|
m.LimitRate(cfg.Options.MaxSendKbps)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GUI
|
// GUI
|
||||||
if opts.GUI && opts.GUIAddr != "" {
|
if cfg.Options.GUIEnabled && cfg.Options.GUIAddress != "" {
|
||||||
host, port, err := net.SplitHostPort(opts.GUIAddr)
|
host, port, err := net.SplitHostPort(cfg.Options.GUIAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnf("Cannot start GUI on %q: %v", opts.GUIAddr, err)
|
warnf("Cannot start GUI on %q: %v", cfg.Options.GUIAddress, err)
|
||||||
} else {
|
} else {
|
||||||
if len(host) > 0 {
|
if len(host) > 0 {
|
||||||
infof("Starting web GUI on http://%s", opts.GUIAddr)
|
infof("Starting web GUI on http://%s", cfg.Options.GUIAddress)
|
||||||
} else {
|
} else {
|
||||||
infof("Starting web GUI on port %s", port)
|
infof("Starting web GUI on port %s", port)
|
||||||
}
|
}
|
||||||
startGUI(opts.GUIAddr, m)
|
startGUI(cfg.Options.GUIAddress, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,22 +212,22 @@ func main() {
|
|||||||
|
|
||||||
// Routine to listen for incoming connections
|
// Routine to listen for incoming connections
|
||||||
infoln("Listening for incoming connections")
|
infoln("Listening for incoming connections")
|
||||||
go listen(myID, opts.Listen, m, cfg)
|
go listen(myID, cfg.Options.ListenAddress, m, tlsCfg)
|
||||||
|
|
||||||
// Routine to connect out to configured nodes
|
// Routine to connect out to configured nodes
|
||||||
infoln("Attempting to connect to other nodes")
|
infoln("Attempting to connect to other nodes")
|
||||||
go connect(myID, opts.Listen, nodeAddrs, m, cfg)
|
go connect(myID, cfg.Options.ListenAddress, m, tlsCfg)
|
||||||
|
|
||||||
// Routine to pull blocks from other nodes to synchronize the local
|
// Routine to pull blocks from other nodes to synchronize the local
|
||||||
// repository. Does not run when we are in read only (publish only) mode.
|
// repository. Does not run when we are in read only (publish only) mode.
|
||||||
if !opts.ReadOnly {
|
if !cfg.Options.ReadOnly {
|
||||||
if opts.Delete {
|
if cfg.Options.AllowDelete {
|
||||||
infoln("Deletes from peer nodes are allowed")
|
infoln("Deletes from peer nodes are allowed")
|
||||||
} else {
|
} else {
|
||||||
infoln("Deletes from peer nodes will be ignored")
|
infoln("Deletes from peer nodes will be ignored")
|
||||||
}
|
}
|
||||||
okln("Ready to synchronize (read-write)")
|
okln("Ready to synchronize (read-write)")
|
||||||
m.StartRW(opts.Delete, opts.ParallelRequests)
|
m.StartRW(cfg.Options.AllowDelete, cfg.Options.ParallelRequests)
|
||||||
} else {
|
} else {
|
||||||
okln("Ready to synchronize (read only; no external updates accepted)")
|
okln("Ready to synchronize (read only; no external updates accepted)")
|
||||||
}
|
}
|
||||||
@@ -219,9 +235,10 @@ func main() {
|
|||||||
// Periodically scan the repository and update the local model.
|
// Periodically scan the repository and update the local model.
|
||||||
// XXX: Should use some fsnotify mechanism.
|
// XXX: Should use some fsnotify mechanism.
|
||||||
go func() {
|
go func() {
|
||||||
|
td := time.Duration(cfg.Options.RescanIntervalS) * time.Second
|
||||||
for {
|
for {
|
||||||
time.Sleep(opts.ScanInterval)
|
time.Sleep(td)
|
||||||
if m.LocalAge() > opts.ScanInterval.Seconds()/2 {
|
if m.LocalAge() > (td / 2).Seconds() {
|
||||||
updateLocalModel(m)
|
updateLocalModel(m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +250,40 @@ func main() {
|
|||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var saveConfigCh = make(chan struct{})
|
||||||
|
|
||||||
|
func saveConfigLoop(cfgFile string) {
|
||||||
|
for _ = range saveConfigCh {
|
||||||
|
fd, err := os.Create(cfgFile + ".tmp")
|
||||||
|
if err != nil {
|
||||||
|
warnln(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writeConfigXML(fd, cfg)
|
||||||
|
if err != nil {
|
||||||
|
warnln(err)
|
||||||
|
fd.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fd.Close()
|
||||||
|
if err != nil {
|
||||||
|
warnln(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Rename(cfgFile+".tmp", cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
warnln(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveConfig() {
|
||||||
|
saveConfigCh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
func printStatsLoop(m *model.Model) {
|
func printStatsLoop(m *model.Model) {
|
||||||
var lastUpdated int64
|
var lastUpdated int64
|
||||||
var lastStats = make(map[string]model.ConnectionInfo)
|
var lastStats = make(map[string]model.ConnectionInfo)
|
||||||
@@ -264,8 +315,8 @@ func printStatsLoop(m *model.Model) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listen(myID string, addr string, m *model.Model, cfg *tls.Config) {
|
func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
|
||||||
l, err := tls.Listen("tcp", addr, cfg)
|
l, err := tls.Listen("tcp", addr, tlsCfg)
|
||||||
fatalErr(err)
|
fatalErr(err)
|
||||||
|
|
||||||
connOpts := map[string]string{
|
connOpts := map[string]string{
|
||||||
@@ -305,8 +356,8 @@ listen:
|
|||||||
warnf("Connect from connected node (%s)", remoteID)
|
warnf("Connect from connected node (%s)", remoteID)
|
||||||
}
|
}
|
||||||
|
|
||||||
for nodeID := range nodeAddrs {
|
for _, nodeCfg := range cfg.Repositories[0].Nodes {
|
||||||
if nodeID == remoteID {
|
if nodeCfg.NodeID == remoteID {
|
||||||
protoConn := protocol.NewConnection(remoteID, conn, conn, m, connOpts)
|
protoConn := protocol.NewConnection(remoteID, conn, conn, m, connOpts)
|
||||||
m.AddConnection(conn, protoConn)
|
m.AddConnection(conn, protoConn)
|
||||||
continue listen
|
continue listen
|
||||||
@@ -316,24 +367,24 @@ listen:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.Model, cfg *tls.Config) {
|
func connect(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
|
||||||
_, portstr, err := net.SplitHostPort(addr)
|
_, portstr, err := net.SplitHostPort(addr)
|
||||||
fatalErr(err)
|
fatalErr(err)
|
||||||
port, _ := strconv.Atoi(portstr)
|
port, _ := strconv.Atoi(portstr)
|
||||||
|
|
||||||
if !opts.LocalDiscovery {
|
if !cfg.Options.LocalAnnEnabled {
|
||||||
port = -1
|
port = -1
|
||||||
} else {
|
} else {
|
||||||
infoln("Sending local discovery announcements")
|
infoln("Sending local discovery announcements")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !opts.ExternalDiscovery {
|
if !cfg.Options.GlobalAnnEnabled {
|
||||||
opts.ExternalServer = ""
|
cfg.Options.GlobalAnnServer = ""
|
||||||
} else {
|
} else {
|
||||||
infoln("Sending external discovery announcements")
|
infoln("Sending external discovery announcements")
|
||||||
}
|
}
|
||||||
|
|
||||||
disc, err := discover.NewDiscoverer(myID, port, opts.ExternalServer)
|
disc, err := discover.NewDiscoverer(myID, port, cfg.Options.GlobalAnnServer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnf("No discovery possible (%v)", err)
|
warnf("No discovery possible (%v)", err)
|
||||||
@@ -346,18 +397,18 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
nextNode:
|
nextNode:
|
||||||
for nodeID, addrs := range nodeAddrs {
|
for _, nodeCfg := range cfg.Repositories[0].Nodes {
|
||||||
if nodeID == myID {
|
if nodeCfg.NodeID == myID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if m.ConnectedTo(nodeID) {
|
if m.ConnectedTo(nodeCfg.NodeID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, addr := range addrs {
|
for _, addr := range nodeCfg.Addresses {
|
||||||
if addr == "dynamic" {
|
if addr == "dynamic" {
|
||||||
var ok bool
|
var ok bool
|
||||||
if disc != nil {
|
if disc != nil {
|
||||||
addr, ok = disc.Lookup(nodeID)
|
addr, ok = disc.Lookup(nodeCfg.NodeID)
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -365,9 +416,9 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(trace, "connect") {
|
if strings.Contains(trace, "connect") {
|
||||||
debugln("NET: Dial", nodeID, addr)
|
debugln("NET: Dial", nodeCfg.NodeID, addr)
|
||||||
}
|
}
|
||||||
conn, err := tls.Dial("tcp", addr, cfg)
|
conn, err := tls.Dial("tcp", addr, tlsCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(trace, "connect") {
|
if strings.Contains(trace, "connect") {
|
||||||
debugln("NET:", err)
|
debugln("NET:", err)
|
||||||
@@ -376,8 +427,8 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
|||||||
}
|
}
|
||||||
|
|
||||||
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
|
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
|
||||||
if remoteID != nodeID {
|
if remoteID != nodeCfg.NodeID {
|
||||||
warnln("Unexpected nodeID", remoteID, "!=", nodeID)
|
warnln("Unexpected nodeID", remoteID, "!=", nodeCfg.NodeID)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -388,12 +439,12 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(opts.ConnInterval)
|
time.Sleep(time.Duration(cfg.Options.ReconnectIntervalS) * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLocalModel(m *model.Model) {
|
func updateLocalModel(m *model.Model) {
|
||||||
files, _ := m.Walk(opts.Symlinks)
|
files, _ := m.Walk(cfg.Options.FollowSymlinks)
|
||||||
m.ReplaceLocal(files)
|
m.ReplaceLocal(files)
|
||||||
saveIndex(m)
|
saveIndex(m)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user